diff --git a/.editorconfig b/.editorconfig index 9bf812d1..a9ff980b 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,513 +1,51 @@ -# SPDX-FileCopyrightText: 2021 Paul Schaub +# This .editorconfig section approximates ktfmt's formatting rules. You can include it in an +# existing .editorconfig file or use it standalone by copying it to /.editorconfig +# and making sure your editor is set to read settings from .editorconfig files. # -# SPDX-License-Identifier: CC0-1.0 +# It includes editor-specific config options for IntelliJ IDEA. +# +# If any option is wrong, PR are welcome -[*] -charset = utf-8 -end_of_line = lf -indent_size = 4 +[{*.kt,*.kts}] indent_style = space -insert_final_newline = false -max_line_length = 120 -tab_width = 4 -ij_continuation_indent_size = 8 -ij_formatter_off_tag = @formatter:off -ij_formatter_on_tag = @formatter:on -ij_formatter_tags_enabled = false -ij_smart_tabs = false -ij_visual_guides = none -ij_wrap_on_typing = false - -[*.java] -ij_java_align_consecutive_assignments = false -ij_java_align_consecutive_variable_declarations = false -ij_java_align_group_field_declarations = false -ij_java_align_multiline_annotation_parameters = false -ij_java_align_multiline_array_initializer_expression = false -ij_java_align_multiline_assignment = false -ij_java_align_multiline_binary_operation = false -ij_java_align_multiline_chained_methods = false -ij_java_align_multiline_extends_list = false -ij_java_align_multiline_for = true -ij_java_align_multiline_method_parentheses = false -ij_java_align_multiline_parameters = true -ij_java_align_multiline_parameters_in_calls = false -ij_java_align_multiline_parenthesized_expression = false -ij_java_align_multiline_records = true -ij_java_align_multiline_resources = true -ij_java_align_multiline_ternary_operation = false -ij_java_align_multiline_text_blocks = false -ij_java_align_multiline_throws_list = false -ij_java_align_subsequent_simple_methods = false -ij_java_align_throws_keyword = false -ij_java_annotation_parameter_wrap = off -ij_java_array_initializer_new_line_after_left_brace = false -ij_java_array_initializer_right_brace_on_new_line = false -ij_java_array_initializer_wrap = off -ij_java_assert_statement_colon_on_next_line = false -ij_java_assert_statement_wrap = off -ij_java_assignment_wrap = off -ij_java_binary_operation_sign_on_next_line = false -ij_java_binary_operation_wrap = off -ij_java_blank_lines_after_anonymous_class_header = 0 -ij_java_blank_lines_after_class_header = 0 -ij_java_blank_lines_after_imports = 1 -ij_java_blank_lines_after_package = 1 -ij_java_blank_lines_around_class = 1 -ij_java_blank_lines_around_field = 0 -ij_java_blank_lines_around_field_in_interface = 0 -ij_java_blank_lines_around_initializer = 1 -ij_java_blank_lines_around_method = 1 -ij_java_blank_lines_around_method_in_interface = 1 -ij_java_blank_lines_before_class_end = 0 -ij_java_blank_lines_before_imports = 1 -ij_java_blank_lines_before_method_body = 0 -ij_java_blank_lines_before_package = 0 -ij_java_block_brace_style = end_of_line -ij_java_block_comment_at_first_column = true -ij_java_builder_methods = none -ij_java_call_parameters_new_line_after_left_paren = false -ij_java_call_parameters_right_paren_on_new_line = false -ij_java_call_parameters_wrap = off -ij_java_case_statement_on_separate_line = true -ij_java_catch_on_new_line = false -ij_java_class_annotation_wrap = split_into_lines -ij_java_class_brace_style = end_of_line -ij_java_class_count_to_use_import_on_demand = 10000 -ij_java_class_names_in_javadoc = 1 -ij_java_do_not_indent_top_level_class_members = false -ij_java_do_not_wrap_after_single_annotation = false -ij_java_do_while_brace_force = never -ij_java_doc_add_blank_line_after_description = true -ij_java_doc_add_blank_line_after_param_comments = false -ij_java_doc_add_blank_line_after_return = false -ij_java_doc_add_p_tag_on_empty_lines = true -ij_java_doc_align_exception_comments = true -ij_java_doc_align_param_comments = true -ij_java_doc_do_not_wrap_if_one_line = false -ij_java_doc_enable_formatting = true -ij_java_doc_enable_leading_asterisks = true -ij_java_doc_indent_on_continuation = false -ij_java_doc_keep_empty_lines = true -ij_java_doc_keep_empty_parameter_tag = true -ij_java_doc_keep_empty_return_tag = true -ij_java_doc_keep_empty_throws_tag = true -ij_java_doc_keep_invalid_tags = true -ij_java_doc_param_description_on_new_line = false -ij_java_doc_preserve_line_breaks = false -ij_java_doc_use_throws_not_exception_tag = true -ij_java_else_on_new_line = false -ij_java_enum_constants_wrap = off -ij_java_extends_keyword_wrap = off -ij_java_extends_list_wrap = off -ij_java_field_annotation_wrap = split_into_lines -ij_java_finally_on_new_line = false -ij_java_for_brace_force = never -ij_java_for_statement_new_line_after_left_paren = false -ij_java_for_statement_right_paren_on_new_line = false -ij_java_for_statement_wrap = off -ij_java_generate_final_locals = false -ij_java_generate_final_parameters = false -ij_java_if_brace_force = never -ij_java_imports_layout = $*,|,java.**,javax.**,|,* -ij_java_indent_case_from_switch = true -ij_java_insert_inner_class_imports = false -ij_java_insert_override_annotation = true -ij_java_keep_blank_lines_before_right_brace = 2 -ij_java_keep_blank_lines_between_package_declaration_and_header = 2 -ij_java_keep_blank_lines_in_code = 2 -ij_java_keep_blank_lines_in_declarations = 2 -ij_java_keep_builder_methods_indents = false -ij_java_keep_control_statement_in_one_line = true -ij_java_keep_first_column_comment = true -ij_java_keep_indents_on_empty_lines = false -ij_java_keep_line_breaks = true -ij_java_keep_multiple_expressions_in_one_line = false -ij_java_keep_simple_blocks_in_one_line = false -ij_java_keep_simple_classes_in_one_line = false -ij_java_keep_simple_lambdas_in_one_line = false -ij_java_keep_simple_methods_in_one_line = false -ij_java_label_indent_absolute = false -ij_java_label_indent_size = 0 -ij_java_lambda_brace_style = end_of_line -ij_java_layout_static_imports_separately = true -ij_java_line_comment_add_space = false -ij_java_line_comment_at_first_column = true -ij_java_method_annotation_wrap = split_into_lines -ij_java_method_brace_style = end_of_line -ij_java_method_call_chain_wrap = off -ij_java_method_parameters_new_line_after_left_paren = false -ij_java_method_parameters_right_paren_on_new_line = false -ij_java_method_parameters_wrap = off -ij_java_modifier_list_wrap = false -ij_java_names_count_to_use_import_on_demand = 1000 -ij_java_new_line_after_lparen_in_record_header = false -ij_java_packages_to_use_import_on_demand = java.awt.*,javax.swing.* -ij_java_parameter_annotation_wrap = off -ij_java_parentheses_expression_new_line_after_left_paren = false -ij_java_parentheses_expression_right_paren_on_new_line = false -ij_java_place_assignment_sign_on_next_line = false -ij_java_prefer_longer_names = true -ij_java_prefer_parameters_wrap = false -ij_java_record_components_wrap = normal -ij_java_repeat_synchronized = true -ij_java_replace_instanceof_and_cast = false -ij_java_replace_null_check = true -ij_java_replace_sum_lambda_with_method_ref = true -ij_java_resource_list_new_line_after_left_paren = false -ij_java_resource_list_right_paren_on_new_line = false -ij_java_resource_list_wrap = off -ij_java_rparen_on_new_line_in_record_header = false -ij_java_space_after_closing_angle_bracket_in_type_argument = false -ij_java_space_after_colon = true -ij_java_space_after_comma = true -ij_java_space_after_comma_in_type_arguments = true -ij_java_space_after_for_semicolon = true -ij_java_space_after_quest = true -ij_java_space_after_type_cast = true -ij_java_space_before_annotation_array_initializer_left_brace = false -ij_java_space_before_annotation_parameter_list = false -ij_java_space_before_array_initializer_left_brace = false -ij_java_space_before_catch_keyword = true -ij_java_space_before_catch_left_brace = true -ij_java_space_before_catch_parentheses = true -ij_java_space_before_class_left_brace = true -ij_java_space_before_colon = true -ij_java_space_before_colon_in_foreach = true -ij_java_space_before_comma = false -ij_java_space_before_do_left_brace = true -ij_java_space_before_else_keyword = true -ij_java_space_before_else_left_brace = true -ij_java_space_before_finally_keyword = true -ij_java_space_before_finally_left_brace = true -ij_java_space_before_for_left_brace = true -ij_java_space_before_for_parentheses = true -ij_java_space_before_for_semicolon = false -ij_java_space_before_if_left_brace = true -ij_java_space_before_if_parentheses = true -ij_java_space_before_method_call_parentheses = false -ij_java_space_before_method_left_brace = true -ij_java_space_before_method_parentheses = false -ij_java_space_before_opening_angle_bracket_in_type_parameter = false -ij_java_space_before_quest = true -ij_java_space_before_switch_left_brace = true -ij_java_space_before_switch_parentheses = true -ij_java_space_before_synchronized_left_brace = true -ij_java_space_before_synchronized_parentheses = true -ij_java_space_before_try_left_brace = true -ij_java_space_before_try_parentheses = true -ij_java_space_before_type_parameter_list = false -ij_java_space_before_while_keyword = true -ij_java_space_before_while_left_brace = true -ij_java_space_before_while_parentheses = true -ij_java_space_inside_one_line_enum_braces = false -ij_java_space_within_empty_array_initializer_braces = false -ij_java_space_within_empty_method_call_parentheses = false -ij_java_space_within_empty_method_parentheses = false -ij_java_spaces_around_additive_operators = true -ij_java_spaces_around_assignment_operators = true -ij_java_spaces_around_bitwise_operators = true -ij_java_spaces_around_equality_operators = true -ij_java_spaces_around_lambda_arrow = true -ij_java_spaces_around_logical_operators = true -ij_java_spaces_around_method_ref_dbl_colon = false -ij_java_spaces_around_multiplicative_operators = true -ij_java_spaces_around_relational_operators = true -ij_java_spaces_around_shift_operators = true -ij_java_spaces_around_type_bounds_in_type_parameters = true -ij_java_spaces_around_unary_operator = false -ij_java_spaces_within_angle_brackets = false -ij_java_spaces_within_annotation_parentheses = false -ij_java_spaces_within_array_initializer_braces = false -ij_java_spaces_within_braces = false -ij_java_spaces_within_brackets = false -ij_java_spaces_within_cast_parentheses = false -ij_java_spaces_within_catch_parentheses = false -ij_java_spaces_within_for_parentheses = false -ij_java_spaces_within_if_parentheses = false -ij_java_spaces_within_method_call_parentheses = false -ij_java_spaces_within_method_parentheses = false -ij_java_spaces_within_parentheses = false -ij_java_spaces_within_record_header = false -ij_java_spaces_within_switch_parentheses = false -ij_java_spaces_within_synchronized_parentheses = false -ij_java_spaces_within_try_parentheses = false -ij_java_spaces_within_while_parentheses = false -ij_java_special_else_if_treatment = true -ij_java_subclass_name_suffix = Impl -ij_java_ternary_operation_signs_on_next_line = false -ij_java_ternary_operation_wrap = off -ij_java_test_name_suffix = Test -ij_java_throws_keyword_wrap = off -ij_java_throws_list_wrap = off -ij_java_use_external_annotations = false -ij_java_use_fq_class_names = false -ij_java_use_relative_indents = false -ij_java_use_single_class_imports = true -ij_java_variable_annotation_wrap = off -ij_java_visibility = public -ij_java_while_brace_force = never -ij_java_while_on_new_line = false -ij_java_wrap_comments = false -ij_java_wrap_first_method_in_call_chain = false -ij_java_wrap_long_lines = false - -[*.properties] -ij_properties_align_group_field_declarations = false -ij_properties_keep_blank_lines = false -ij_properties_key_value_delimiter = equals -ij_properties_spaces_around_key_value_delimiter = false - -[.editorconfig] -ij_editorconfig_align_group_field_declarations = false -ij_editorconfig_space_after_colon = false -ij_editorconfig_space_after_comma = true -ij_editorconfig_space_before_colon = false -ij_editorconfig_space_before_comma = false -ij_editorconfig_spaces_around_assignment_operators = true - -[{*.ant,*.fxml,*.jhm,*.jnlp,*.jrxml,*.jspx,*.pom,*.rng,*.tagx,*.tld,*.wsdl,*.xml,*.xsd,*.xsl,*.xslt,*.xul}] -ij_xml_align_attributes = true -ij_xml_align_text = false -ij_xml_attribute_wrap = normal -ij_xml_block_comment_at_first_column = true -ij_xml_keep_blank_lines = 2 -ij_xml_keep_indents_on_empty_lines = false -ij_xml_keep_line_breaks = true -ij_xml_keep_line_breaks_in_text = true -ij_xml_keep_whitespaces = false -ij_xml_keep_whitespaces_around_cdata = preserve -ij_xml_keep_whitespaces_inside_cdata = false -ij_xml_line_comment_at_first_column = true -ij_xml_space_after_tag_name = false -ij_xml_space_around_equals_in_attribute = false -ij_xml_space_inside_empty_tag = false -ij_xml_text_wrap = normal -ij_xml_use_custom_settings = false - -[{*.bash,*.sh,*.zsh}] -indent_size = 2 -tab_width = 2 -ij_shell_binary_ops_start_line = false -ij_shell_keep_column_alignment_padding = false -ij_shell_minify_program = false -ij_shell_redirect_followed_by_space = false -ij_shell_switch_cases_indented = false -ij_shell_use_unix_line_separator = true - -[{*.gant,*.gradle,*.groovy,*.gy}] -ij_groovy_align_group_field_declarations = false -ij_groovy_align_multiline_array_initializer_expression = false -ij_groovy_align_multiline_assignment = false -ij_groovy_align_multiline_binary_operation = false -ij_groovy_align_multiline_chained_methods = false -ij_groovy_align_multiline_extends_list = false -ij_groovy_align_multiline_for = true -ij_groovy_align_multiline_list_or_map = true -ij_groovy_align_multiline_method_parentheses = false -ij_groovy_align_multiline_parameters = true -ij_groovy_align_multiline_parameters_in_calls = false -ij_groovy_align_multiline_resources = true -ij_groovy_align_multiline_ternary_operation = false -ij_groovy_align_multiline_throws_list = false -ij_groovy_align_named_args_in_map = true -ij_groovy_align_throws_keyword = false -ij_groovy_array_initializer_new_line_after_left_brace = false -ij_groovy_array_initializer_right_brace_on_new_line = false -ij_groovy_array_initializer_wrap = off -ij_groovy_assert_statement_wrap = off -ij_groovy_assignment_wrap = off -ij_groovy_binary_operation_wrap = off -ij_groovy_blank_lines_after_class_header = 0 -ij_groovy_blank_lines_after_imports = 1 -ij_groovy_blank_lines_after_package = 1 -ij_groovy_blank_lines_around_class = 1 -ij_groovy_blank_lines_around_field = 0 -ij_groovy_blank_lines_around_field_in_interface = 0 -ij_groovy_blank_lines_around_method = 1 -ij_groovy_blank_lines_around_method_in_interface = 1 -ij_groovy_blank_lines_before_imports = 1 -ij_groovy_blank_lines_before_method_body = 0 -ij_groovy_blank_lines_before_package = 0 -ij_groovy_block_brace_style = end_of_line -ij_groovy_block_comment_at_first_column = true -ij_groovy_call_parameters_new_line_after_left_paren = false -ij_groovy_call_parameters_right_paren_on_new_line = false -ij_groovy_call_parameters_wrap = off -ij_groovy_catch_on_new_line = false -ij_groovy_class_annotation_wrap = split_into_lines -ij_groovy_class_brace_style = end_of_line -ij_groovy_class_count_to_use_import_on_demand = 5 -ij_groovy_do_while_brace_force = never -ij_groovy_else_on_new_line = false -ij_groovy_enum_constants_wrap = off -ij_groovy_extends_keyword_wrap = off -ij_groovy_extends_list_wrap = off -ij_groovy_field_annotation_wrap = split_into_lines -ij_groovy_finally_on_new_line = false -ij_groovy_for_brace_force = never -ij_groovy_for_statement_new_line_after_left_paren = false -ij_groovy_for_statement_right_paren_on_new_line = false -ij_groovy_for_statement_wrap = off -ij_groovy_if_brace_force = never -ij_groovy_import_annotation_wrap = 2 -ij_groovy_imports_layout = *,|,javax.**,java.**,|,$* -ij_groovy_indent_case_from_switch = true -ij_groovy_indent_label_blocks = true -ij_groovy_insert_inner_class_imports = false -ij_groovy_keep_blank_lines_before_right_brace = 2 -ij_groovy_keep_blank_lines_in_code = 2 -ij_groovy_keep_blank_lines_in_declarations = 2 -ij_groovy_keep_control_statement_in_one_line = true -ij_groovy_keep_first_column_comment = true -ij_groovy_keep_indents_on_empty_lines = false -ij_groovy_keep_line_breaks = true -ij_groovy_keep_multiple_expressions_in_one_line = false -ij_groovy_keep_simple_blocks_in_one_line = false -ij_groovy_keep_simple_classes_in_one_line = true -ij_groovy_keep_simple_lambdas_in_one_line = true -ij_groovy_keep_simple_methods_in_one_line = true -ij_groovy_label_indent_absolute = false -ij_groovy_label_indent_size = 0 -ij_groovy_lambda_brace_style = end_of_line -ij_groovy_layout_static_imports_separately = true -ij_groovy_line_comment_add_space = false -ij_groovy_line_comment_at_first_column = true -ij_groovy_method_annotation_wrap = split_into_lines -ij_groovy_method_brace_style = end_of_line -ij_groovy_method_call_chain_wrap = off -ij_groovy_method_parameters_new_line_after_left_paren = false -ij_groovy_method_parameters_right_paren_on_new_line = false -ij_groovy_method_parameters_wrap = off -ij_groovy_modifier_list_wrap = false -ij_groovy_names_count_to_use_import_on_demand = 3 -ij_groovy_parameter_annotation_wrap = off -ij_groovy_parentheses_expression_new_line_after_left_paren = false -ij_groovy_parentheses_expression_right_paren_on_new_line = false -ij_groovy_prefer_parameters_wrap = false -ij_groovy_resource_list_new_line_after_left_paren = false -ij_groovy_resource_list_right_paren_on_new_line = false -ij_groovy_resource_list_wrap = off -ij_groovy_space_after_assert_separator = true -ij_groovy_space_after_colon = true -ij_groovy_space_after_comma = true -ij_groovy_space_after_comma_in_type_arguments = true -ij_groovy_space_after_for_semicolon = true -ij_groovy_space_after_quest = true -ij_groovy_space_after_type_cast = true -ij_groovy_space_before_annotation_parameter_list = false -ij_groovy_space_before_array_initializer_left_brace = false -ij_groovy_space_before_assert_separator = false -ij_groovy_space_before_catch_keyword = true -ij_groovy_space_before_catch_left_brace = true -ij_groovy_space_before_catch_parentheses = true -ij_groovy_space_before_class_left_brace = true -ij_groovy_space_before_closure_left_brace = true -ij_groovy_space_before_colon = true -ij_groovy_space_before_comma = false -ij_groovy_space_before_do_left_brace = true -ij_groovy_space_before_else_keyword = true -ij_groovy_space_before_else_left_brace = true -ij_groovy_space_before_finally_keyword = true -ij_groovy_space_before_finally_left_brace = true -ij_groovy_space_before_for_left_brace = true -ij_groovy_space_before_for_parentheses = true -ij_groovy_space_before_for_semicolon = false -ij_groovy_space_before_if_left_brace = true -ij_groovy_space_before_if_parentheses = true -ij_groovy_space_before_method_call_parentheses = false -ij_groovy_space_before_method_left_brace = true -ij_groovy_space_before_method_parentheses = false -ij_groovy_space_before_quest = true -ij_groovy_space_before_switch_left_brace = true -ij_groovy_space_before_switch_parentheses = true -ij_groovy_space_before_synchronized_left_brace = true -ij_groovy_space_before_synchronized_parentheses = true -ij_groovy_space_before_try_left_brace = true -ij_groovy_space_before_try_parentheses = true -ij_groovy_space_before_while_keyword = true -ij_groovy_space_before_while_left_brace = true -ij_groovy_space_before_while_parentheses = true -ij_groovy_space_in_named_argument = true -ij_groovy_space_in_named_argument_before_colon = false -ij_groovy_space_within_empty_array_initializer_braces = false -ij_groovy_space_within_empty_method_call_parentheses = false -ij_groovy_spaces_around_additive_operators = true -ij_groovy_spaces_around_assignment_operators = true -ij_groovy_spaces_around_bitwise_operators = true -ij_groovy_spaces_around_equality_operators = true -ij_groovy_spaces_around_lambda_arrow = true -ij_groovy_spaces_around_logical_operators = true -ij_groovy_spaces_around_multiplicative_operators = true -ij_groovy_spaces_around_regex_operators = true -ij_groovy_spaces_around_relational_operators = true -ij_groovy_spaces_around_shift_operators = true -ij_groovy_spaces_within_annotation_parentheses = false -ij_groovy_spaces_within_array_initializer_braces = false -ij_groovy_spaces_within_braces = true -ij_groovy_spaces_within_brackets = false -ij_groovy_spaces_within_cast_parentheses = false -ij_groovy_spaces_within_catch_parentheses = false -ij_groovy_spaces_within_for_parentheses = false -ij_groovy_spaces_within_gstring_injection_braces = false -ij_groovy_spaces_within_if_parentheses = false -ij_groovy_spaces_within_list_or_map = false -ij_groovy_spaces_within_method_call_parentheses = false -ij_groovy_spaces_within_method_parentheses = false -ij_groovy_spaces_within_parentheses = false -ij_groovy_spaces_within_switch_parentheses = false -ij_groovy_spaces_within_synchronized_parentheses = false -ij_groovy_spaces_within_try_parentheses = false -ij_groovy_spaces_within_tuple_expression = false -ij_groovy_spaces_within_while_parentheses = false -ij_groovy_special_else_if_treatment = true -ij_groovy_ternary_operation_wrap = off -ij_groovy_throws_keyword_wrap = off -ij_groovy_throws_list_wrap = off -ij_groovy_use_flying_geese_braces = false -ij_groovy_use_fq_class_names = false -ij_groovy_use_fq_class_names_in_javadoc = true -ij_groovy_use_relative_indents = false -ij_groovy_use_single_class_imports = true -ij_groovy_variable_annotation_wrap = off -ij_groovy_while_brace_force = never -ij_groovy_while_on_new_line = false -ij_groovy_wrap_long_lines = false - -[{*.gradle.kts,*.kt,*.kts,*.main.kts,*.space.kts}] +insert_final_newline = true +max_line_length = 100 +indent_size = 4 +ij_continuation_indent_size = 4 # was 8 +ij_java_names_count_to_use_import_on_demand = 9999 ij_kotlin_align_in_columns_case_branch = false ij_kotlin_align_multiline_binary_operation = false ij_kotlin_align_multiline_extends_list = false ij_kotlin_align_multiline_method_parentheses = false ij_kotlin_align_multiline_parameters = true ij_kotlin_align_multiline_parameters_in_calls = false -ij_kotlin_allow_trailing_comma = false -ij_kotlin_allow_trailing_comma_on_call_site = false -ij_kotlin_assignment_wrap = off +ij_kotlin_allow_trailing_comma = true +ij_kotlin_allow_trailing_comma_on_call_site = true +ij_kotlin_assignment_wrap = normal ij_kotlin_blank_lines_after_class_header = 0 ij_kotlin_blank_lines_around_block_when_branches = 0 ij_kotlin_blank_lines_before_declaration_with_comment_or_annotation_on_separate_line = 1 ij_kotlin_block_comment_at_first_column = true -ij_kotlin_call_parameters_new_line_after_left_paren = false +ij_kotlin_call_parameters_new_line_after_left_paren = true ij_kotlin_call_parameters_right_paren_on_new_line = false -ij_kotlin_call_parameters_wrap = off +ij_kotlin_call_parameters_wrap = on_every_item ij_kotlin_catch_on_new_line = false ij_kotlin_class_annotation_wrap = split_into_lines +ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL ij_kotlin_continuation_indent_for_chained_calls = true ij_kotlin_continuation_indent_for_expression_bodies = true ij_kotlin_continuation_indent_in_argument_lists = true -ij_kotlin_continuation_indent_in_elvis = true -ij_kotlin_continuation_indent_in_if_conditions = true -ij_kotlin_continuation_indent_in_parameter_lists = true -ij_kotlin_continuation_indent_in_supertype_lists = true +ij_kotlin_continuation_indent_in_elvis = false +ij_kotlin_continuation_indent_in_if_conditions = false +ij_kotlin_continuation_indent_in_parameter_lists = false +ij_kotlin_continuation_indent_in_supertype_lists = false ij_kotlin_else_on_new_line = false ij_kotlin_enum_constants_wrap = off -ij_kotlin_extends_list_wrap = off +ij_kotlin_extends_list_wrap = normal ij_kotlin_field_annotation_wrap = split_into_lines ij_kotlin_finally_on_new_line = false ij_kotlin_if_rparen_on_new_line = false ij_kotlin_import_nested_classes = false -ij_kotlin_imports_layout = *,java.**,javax.**,kotlin.**,^ ij_kotlin_insert_whitespaces_in_simple_one_line_method = true ij_kotlin_keep_blank_lines_before_right_brace = 2 ij_kotlin_keep_blank_lines_in_code = 2 @@ -519,13 +57,13 @@ ij_kotlin_lbrace_on_next_line = false ij_kotlin_line_comment_add_space = false ij_kotlin_line_comment_at_first_column = true ij_kotlin_method_annotation_wrap = split_into_lines -ij_kotlin_method_call_chain_wrap = off -ij_kotlin_method_parameters_new_line_after_left_paren = false -ij_kotlin_method_parameters_right_paren_on_new_line = false -ij_kotlin_method_parameters_wrap = off -ij_kotlin_name_count_to_use_star_import = 5 -ij_kotlin_name_count_to_use_star_import_for_members = 3 -ij_kotlin_packages_to_use_import_on_demand = java.util.*,kotlinx.android.synthetic.**,io.ktor.** +ij_kotlin_method_call_chain_wrap = normal +ij_kotlin_method_parameters_new_line_after_left_paren = true +ij_kotlin_method_parameters_right_paren_on_new_line = true +ij_kotlin_method_parameters_wrap = on_every_item +ij_kotlin_name_count_to_use_star_import = 9999 +ij_kotlin_name_count_to_use_star_import_for_members = 9999 +ij_java_names_count_to_use_import_on_demand = 9999 ij_kotlin_parameter_annotation_wrap = off ij_kotlin_space_after_comma = true ij_kotlin_space_after_extend_colon = true @@ -552,72 +90,5 @@ ij_kotlin_spaces_around_when_arrow = true ij_kotlin_variable_annotation_wrap = off ij_kotlin_while_on_new_line = false ij_kotlin_wrap_elvis_expressions = 1 -ij_kotlin_wrap_expression_body_functions = 0 +ij_kotlin_wrap_expression_body_functions = 1 ij_kotlin_wrap_first_method_in_call_chain = false - -[{*.har,*.json}] -indent_size = 2 -ij_json_keep_blank_lines_in_code = 0 -ij_json_keep_indents_on_empty_lines = false -ij_json_keep_line_breaks = true -ij_json_space_after_colon = true -ij_json_space_after_comma = true -ij_json_space_before_colon = true -ij_json_space_before_comma = false -ij_json_spaces_within_braces = false -ij_json_spaces_within_brackets = false -ij_json_wrap_long_lines = false - -[{*.htm,*.html,*.sht,*.shtm,*.shtml}] -ij_html_add_new_line_before_tags = body,div,p,form,h1,h2,h3 -ij_html_align_attributes = true -ij_html_align_text = false -ij_html_attribute_wrap = normal -ij_html_block_comment_at_first_column = true -ij_html_do_not_align_children_of_min_lines = 0 -ij_html_do_not_break_if_inline_tags = title,h1,h2,h3,h4,h5,h6,p -ij_html_do_not_indent_children_of_tags = html,body,thead,tbody,tfoot -ij_html_enforce_quotes = false -ij_html_inline_tags = a,abbr,acronym,b,basefont,bdo,big,br,cite,cite,code,dfn,em,font,i,img,input,kbd,label,q,s,samp,select,small,span,strike,strong,sub,sup,textarea,tt,u,var -ij_html_keep_blank_lines = 2 -ij_html_keep_indents_on_empty_lines = false -ij_html_keep_line_breaks = true -ij_html_keep_line_breaks_in_text = true -ij_html_keep_whitespaces = false -ij_html_keep_whitespaces_inside = span,pre,textarea -ij_html_line_comment_at_first_column = true -ij_html_new_line_after_last_attribute = never -ij_html_new_line_before_first_attribute = never -ij_html_quote_style = double -ij_html_remove_new_line_before_tags = br -ij_html_space_after_tag_name = false -ij_html_space_around_equality_in_attribute = false -ij_html_space_inside_empty_tag = false -ij_html_text_wrap = normal -ij_html_uniform_ident = false - -[{*.markdown,*.md}] -ij_markdown_force_one_space_after_blockquote_symbol = true -ij_markdown_force_one_space_after_header_symbol = true -ij_markdown_force_one_space_after_list_bullet = true -ij_markdown_force_one_space_between_words = true -ij_markdown_keep_indents_on_empty_lines = false -ij_markdown_max_lines_around_block_elements = 1 -ij_markdown_max_lines_around_header = 1 -ij_markdown_max_lines_between_paragraphs = 1 -ij_markdown_min_lines_around_block_elements = 1 -ij_markdown_min_lines_around_header = 1 -ij_markdown_min_lines_between_paragraphs = 1 - -[{*.yaml,*.yml}] -indent_size = 2 -ij_yaml_align_values_properties = do_not_align -ij_yaml_autoinsert_sequence_marker = true -ij_yaml_block_mapping_on_new_line = false -ij_yaml_indent_sequence_value = true -ij_yaml_keep_indents_on_empty_lines = false -ij_yaml_keep_line_breaks = true -ij_yaml_sequence_on_new_line = false -ij_yaml_space_before_colon = false -ij_yaml_spaces_within_braces = true -ij_yaml_spaces_within_brackets = true diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 00000000..447355cc --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,2 @@ +# Ignore initial spotlessApply using ktfmt +51e9bfc67f19e16a69790a8d92bd6b1c86a76a5f diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..36f6550a --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: 2021 Paul Schaub +# +# SPDX-License-Identifier: Apache-2.0 + +# These are supported funding model platforms + +github: vanitasvitae # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +#patreon: # Replace with a single Patreon username +#open_collective: # Replace with a single Open Collective username +#ko_fi: # Replace with a single Ko-fi username +#tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +#community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +#liberapay: # Replace with a single Liberapay username +#issuehunt: # Replace with a single IssueHunt username +#otechie: # Replace with a single Otechie username +#lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry +#custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.github/ISSUE_TEMPLATE/cli-application.md b/.github/ISSUE_TEMPLATE/cli-application.md new file mode 100644 index 00000000..68f35b74 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/cli-application.md @@ -0,0 +1,35 @@ +--- +name: CLI Application +about: Report an issue with the pgpainless-cli utility +title: '' +labels: 'module: cli' +assignees: '' + +--- + +**Describe the bug** + + +**Version** + +- `pgpainless-cli`: + +**Installation Source** + +- Debian Repository +- Built locally (`gradle build...`) + +**To Reproduce** + +1. `pgpainless-cli foo bar [...]`' +2. ... + +**Expected behavior** + + +**Additional context** + +``` +-----BEGIN PGP FOO BAR----- +... +``` diff --git a/.github/ISSUE_TEMPLATE/library.md b/.github/ISSUE_TEMPLATE/library.md new file mode 100644 index 00000000..74f5f666 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/library.md @@ -0,0 +1,28 @@ +--- +name: Library +about: Report an issue with the libraries pgpainless-core or pgpainless-sop +title: '' +labels: 'module: core' +assignees: '' + +--- + +**Describe the bug** + + +**Version** + +- `pgpainless-core`: +- `pgpainless-sop`: + +**To Reproduce** + +``` +Example Code Block with your Code +``` + +**Expected behavior** + + +**Additional context** + diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 00000000..d45b16a3 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,74 @@ +# SPDX-FileCopyrightText: 2021 Paul Schaub +# +# SPDX-License-Identifier: Apache-2.0 + +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ main, release/* ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ main ] + schedule: + - cron: '16 10 * * 0' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'java-kotlin' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Learn more about CodeQL language support at https://git.io/codeql-language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/depencency-submission.yml b/.github/workflows/depencency-submission.yml new file mode 100644 index 00000000..6355c2ba --- /dev/null +++ b/.github/workflows/depencency-submission.yml @@ -0,0 +1,27 @@ +# SPDX-FileCopyrightText: 2023 Paul Schaub +# +# SPDX-License-Identifier: Apache-2.0 + +name: Dependencies +on: + push: + +jobs: + build: + name: Dependencies + runs-on: ubuntu-latest + permissions: # The Dependency Submission API requires write permission + contents: write + steps: + - name: 'Checkout Repository' + uses: actions/checkout@v3 + + - name: Run snapshot action + uses: mikepenz/gradle-dependency-submission@v0.8.6 + with: + gradle-build-module: |- + :pgpainless-core + :pgpainless-sop + :pgpainless-cli + sub-module-mode: |- + INDIVIDUAL_DEEP diff --git a/.github/workflows/gradle_push.yml b/.github/workflows/gradle_push.yml new file mode 100644 index 00000000..8c03a399 --- /dev/null +++ b/.github/workflows/gradle_push.yml @@ -0,0 +1,40 @@ +# SPDX-FileCopyrightText: 2021 Paul Schaub +# +# SPDX-License-Identifier: Apache-2.0 + +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. +# This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time +# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle + +name: Push + +on: + push: + branches: [ "main" ] + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up JDK 11 + uses: actions/setup-java@v3 + with: + java-version: '11' + distribution: 'temurin' + - name: Build and Check + uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1 + with: + arguments: check jacocoRootReport + - name: Coveralls + uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1 + env: + COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} + with: + arguments: coveralls diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml new file mode 100644 index 00000000..4d22c22f --- /dev/null +++ b/.github/workflows/pr.yml @@ -0,0 +1,33 @@ +# SPDX-FileCopyrightText: 2021 Paul Schaub +# +# SPDX-License-Identifier: Apache-2.0 + +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. +# This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time +# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle + +name: PR + +on: + pull_request: + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up JDK 11 + uses: actions/setup-java@v3 + with: + java-version: '11' + distribution: 'temurin' + - name: Build, Check and Coverage + uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1 + with: + arguments: check jacocoRootReport diff --git a/.gitignore b/.gitignore index 84123d97..0a0ff0f1 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,8 @@ libs/ *.log *.jar +local.properties + gradle.properties !gradle-wrapper.jar @@ -29,3 +31,5 @@ pgpainless-core/.project pgpainless-core/.settings/ push_html.sh + +node_modules diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 00000000..1d6088af --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,40 @@ +# .readthedocs.yaml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Set the version of Python and other tools you might need +build: + os: ubuntu-20.04 + # apt_packages: + # - libgtk-3-0 + # - libasound2 + # - libnss3 + # - libxss1 + # - libgbm1 + # - libxshmfence1 + tools: + python: "3.9" + # You can also specify other tool versions: + # nodejs: "16" + # rust: "1.55" + # golang: "1.17" + # jobs: + # post_install: + # - npm install -g @mermaid-js/mermaid-cli + +# Build documentation in the docs/ directory with Sphinx +sphinx: + configuration: docs/source/conf.py + +# If using Sphinx, optionally build your docs in additional formats such as PDF +formats: + - pdf + - epub + +# Optionally declare the Python requirements required to build your docs +python: + install: + - requirements: docs/requirements.txt diff --git a/.reuse/dep5 b/.reuse/dep5 deleted file mode 100644 index 99b0abe2..00000000 --- a/.reuse/dep5 +++ /dev/null @@ -1,32 +0,0 @@ -Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ -Upstream-Name: PGPainless -Upstream-Contact: Paul Schaub -Source: https://pgpainless.org - -# Sample paragraph, commented out: -# -# Files: src/* -# Copyright: $YEAR $NAME <$CONTACT> -# License: ... - -# Gradle build tool -Files: gradle* -Copyright: 2015 the original author or authors. -License: Apache-2.0 - -# PGPainless Logo -Files: assets/repository-open-graph.png -Copyright: 2021 Paul Schaub -License: CC-BY-3.0 - -Files: assets/test_vectors/* -Copyright: 2018 Paul Schaub -License: CC0-1.0 - -Files: pgpainless-core/src/test/resources/* -Copyright: 2020 Paul Schaub -License: CC0-1.0 - -Files: audit/* -Copyright: 2021 Paul Schaub -License: CC0-1.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index f4bb3e49..13c93c07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,9 +5,644 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog +## 1.7.7-SNAPSHOT +- Bump `bcpg-jdk8on` to `1.81` +- Bump `bcprov-jdk18on` to `1.81` + +## 1.7.6 +- Fix `RevocationSignatureBuilder` properly calculating third-party signatures of type `KeyRevocation` (delegation revocations) +- Enable support for native images +- Re-enable shadow plugin and build fat-jar + +## 1.7.5 +- Actually attempt to fix Kotlin desugaring. +- Bump javaSourceCompatibility and javaTargetCompatibility to 11 +- Bump gradle-wrapper to 8.8 + +## 1.7.4 +- Fix proper Kotlin desugaring for Java 8 + +## 1.7.3 +- Bump `bcpg-jdk8on` to `1.80` +- Bump `bcprov-jdk18on` to `1.80` +- Add dependency on `bcutil-jdk18on` as a workaround +- Ignore unknown type signatures on certificates +- Fix typo on signature creation bounds check (thanks @elduffy) +- Fix superfluous newline added in CRLF encoding (thanks @bjansen) +- Bump `sop-java` to `1.10.0` + - SOP inline-sign: Do not apply compression + +## 1.7.2 +- Fix bug in `KeyRingInfo.lastModified` (thanks to @Jerbell, @sosnovsky for reporting) +- Bump `sop-java` to `10.0.3` + - allow multiple arguments `--with-key-password` in `revoke-key` command + - Properly pass `--old-key-password` and `--new-key-password` options as indirect arguments in `change-key-password` command + +## 1.7.1 +- Bump `sop-java` to `10.0.2` +- Downgrade `logback-core` and `logback-classic` to `1.2.13` (fix CLI spam) + +## 1.7.0 +- Bump `bcpg-jdk8on` to `1.78.1` +- Bump `bcprov-jdk18on` to `1.78.1` +- Bump `logback-core` and `logback-classic` to `1.4.14` +- `pgpainless-core` + - Rewrote most of the codebase in Kotlin + - Removed `OpenPgpMetadata` (`decryptionStream.getResult()`) in favor of `MessageMetadata` (`decryptionStream.getMetadata()`) + - Removed support for generating EC keys over non-standard curve `secp256k1` + - Properly feed EOS tokens to the pushdown automaton when reaching the end of stream (thanks @iNPUTmice) + - Do not choke on unknown signature subpackets (thanks @Jerbell) + - Prevent timing issues resulting in subkey binding signatures predating the subkey (@thanks Jerbell) + - Rename LibrePGP-related `Feature` enums: + - `GNUPG_AEAD_ENCRYPTED_DATA` -> `LIBREPGP_OCB_ENCRYPTED_DATA` + - `GNUPG_VERSION_5_PUBLIC_KEY` -> `LIBREPGP_VERSION_5_PUBLIC_KEY` + - Properly reject signatures by non-signing primary keys + - Add `EncryptionBuilder.discardOutput()` (useful for detached signing) + - Remove support for generation of keys over non-standard `secp256k1` curve + - Add base support for padding packets + - Do not choke on LibrePGP OED packets + - Supersede `addPassphrase()`/`addDecryptionPassphrase()` methods with more clear `addMessagePassphrase()` +- `pgpainless-sop`, `pgpainless-cli` + - Bump `sop-java` to `10.0.1`, implementing [SOP Spec Revision 10](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-10.html) + - Change API of `sop.encrypt` to return a `ReadyWithResult` to expose the session key + - `decrypt --verify-with`: Fix to not throw `NoSignature` exception (exit code 3) if `VERIFICATIONS` is empty + - Separate signature verification operations into `SOPV` interface + - Add `version --sopv` option + - Throw `BadData` error when passing KEYS where CERTS are expected. + - `armor`: Remove `--label` option + +## 1.6.8 +- Bump `sop-java` to `7.0.2` +- SOP `change-key-password`: Fix reading password from indirect parameter instead of erroneously passing filename (fixes #453) +- SOP `revoke-key`: Allow for multiple `--with-key-password` options + +## 1.6.7 +- SOP: Fix OOM error when detached-signing large amounts of data (fix #432) +- Move `CachingBcPublicKeyDataDecryptorFactory` from `org.bouncycastle` packet to `org.pgpainless.decryption_verification` to avoid package split (partially addresses #428) +- Basic support for Java Modules for `pgpainless-core` and `pgpainless-sop` + - Added `Automatic-Module-Name` directive to gradle build files + +## 1.6.6 +- Downgrade `logback-core` and `logback-classic` to `1.2.13` to fix #426 + +## 1.6.5 +- Add `SecretKeyRingEditor.setExpirationDateOfSubkey()` + +## 1.6.4 +- Bump `bcpg-jdk8on` to `1.77` +- Bump `bcprov-jdk18on` to `1.77` +- Bump `logback-core` and `logback-classic` to `1.4.13` +- Properly feed EOS tokens to the pushdown automaton when reaching the end of stream (thanks @iNPUTmice) +- Do not choke on unknown signature subpackets (thanks @Jerbell) +- Prevent timing issues resuting in subkey binding signatures predating the subkey (@thanks Jerbell) + +## 1.6.3 +- Bump `sop-java` to `7.0.1` +- `decrypt --verify-with`: Fix to not throw `NoSignature` exception (exit code 3) if `VERIFICATIONS` is empty + +## 1.6.2 +- Switch `bcpg` and `bcprov` artifacts from `-jdk15to18`variant to `-jdk18on` +- Bump `bcpg-jdk8on` to `1.76` +- Bump `bcprov-jdk18on` to `1.76` +- Add `EncryptionOptions.setAllowEncryptionWithMissingKeyFlags()` to properly allow + encrypting to legacy keys which do not carry any key flags. +- Allow overriding of reference time in `EncryptionOptions` and `SigningOptions`. + +## 1.6.1 +- `KeyRingBuilder`: Require UTF8 when adding user-ID via `addUserId(byte[])` +- `pgpainless-sop`: Remove dependency on jetbrains annotations +- Add `CertificateAuthority` interface to allow integration with [`pgpainless-wot`](https://github.com/pgpainless/pgpainless-wot) + - Add `EncryptionOptions.addAuthenticatableRecipients()` method + - Add `MessageMetadata.isAuthenticatablySignedBy()` method + +## 1.6.0 +- Bump `sop-java` to `7.0.0`, implementing [SOP Spec Revision 07](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-07.html) + - Implement `revoke-key` subcommand and API + - Implement `change-key-password` subcommand and API + - `generate-key`: Add support for new `--signing-only` option +- Move some methods related to password changing from `SecretKeyRingEditor` to `KeyRingUtils` + +## 1.5.7 +- Bump `sop-java` to `6.1.1` +- `decrypt --verify-with`: Fix to not throw `NoSignature` exception (exit code 3) if `VERIFICATIONS` is empty + +## 1.5.6 +- Bump `jacoco` to `0.8.8` (thanks @hkos) +- Ignore malformed, non-UTF8 user-IDs on certificates +- `KeyRingReader.readPublicKeyRingCollection()`: Extract and return public keys from encountered secret keys +- Add some utility methods to `KeyRingInfo`: + - `getValidSubkeys()` only returns validly bound sub-keys +- Add some utility methods to `SignatureUtils`: + - `getDelegations()` returns all third-party signatures made over the primary key + - `get3rdPartyCertificationsFor(userId)` returns all third-party certification signatures made over the given user-id +- Add some utility methods to `SignatureSubpacketsUtil`: + - `isExportable()` will return true if the signature is *not* marked as non-exportable + - `getTrustDepthOr()` returns the signatures trust-depth, or a default value if there is no trust-signature subpacket + - `getTrustAmountOr()` returns the signatures trust-amount, or a default value if there is no trust-signature subpacket + +## 1.5.5 +- Bump `bcpg-jdk15to18` to `1.75` +- Bump `bcprov-jdk15to18` to `1.75` +- Bump `checkstyle` to `10.12.1` to fix build dependency on [vulnerable guava](https://github.com/pgpainless/pgpainless/security/dependabot/6). +- `SecretKeyRingEditor`: + - Rename `createRevocationCertificate()` to `createRevocation()` + - Add `createMinimalRevocationCertificate()` method to generate OpenPGP v6-style self-certifying revocation certificates + +## 1.5.4 +- Bump `bcpg-jdk15to18` to `1.74` +- Bump `bcprov-jdk15to18` to `1.74` +- Remove unused methods from `SignatureUtils` +- Encryption: Allow anonymous recipients using wildcard key-IDs +- Add `SignatureSubpacketsUtil.getRegularExpressions()` +- Tests, tests, tests + +## 1.5.3 +- Fix minimal bit-strength check for signing-subkeys accidentally comparing the bit-strength of the primary key +- `SigningOptions`: Add new methods to add signatures using a single, chosen signing subkey + +## 1.5.2 +- Bugfix: Create proper direct-key signatures +- `KeyRingTemplates`: + - Add `rsaKeyRing()` for generating RSA keys with primary key and dedicated signing, encryption subkeys + - Reduce number of template methods by replacing `UserId`, `String` arguments with `CharSequence` +- Add `MessageMetadata.getRecipientKeyIds()` +- Work towards more null-safe API by annotating methods in `EncryptionOptions`, `SigningOptions`, `KeyRingInfo`, `PGPainless` with `@Nonnull`, `@Nullable` +- `KeyRingUtils`: Removed `removeSecretKey()` in favour of `stripSecretKey()` +- General code cleanup +- SOP: generating keys with `rfc4880` profile now generates key with primary key and subkeys +- Deprecate ElGamal key type +- Key generation: Set expiration period of 5 years by default +- Set AES-128 as default fallback symmetric algorithm +- `ProducerOptions`: Allow setting custom version header when encrypting/signing message + +## 1.5.2-rc1 +- Bump `sop-java` to `6.1.0` +- Normalize `OpenPgpMessageInputStream.read()` behaviour when reading past the stream + - Instead of throwing a `MalformedOpenPgpMessageException` which could throw off unsuspecting parsers, + we now simply return `-1` like every other `InputStream`. + +## 1.5.1 +- SOP: Emit signature `mode:{binary|text}` in `Verification` results +- core: Relax constraints on decryption subkeys to improve interoperability with broken clients + - Allow decryption with revoked keys + - Allow decryption with expired keys + - Allow decryption with erroneously addressed keys without encryption key flags + +## 1.5.0 +- Bump `bcpg-jdk15to18` to `1.73` +- Bump `bcprov-jdk15to18` to `1.73` +- Introduce `OpenPgpv6Fingerprint` class +- Bump `sop-java` to `5.0.0`, implementing [SOP Spec Revision 05](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-05.html) + - Add support for `list-profiles` subcommand (`generate-key` only for now) + - `generate-key`: Add support for `--profile=` option + - Add profile `draft-koch-eddsa-for-openpgp-00` which represents status quo. + - Add profile `rfc4880` which generates keys based on 4096-bit RSA. +- Bump `sop-java` to `6.0.0`, implementing [SOP Spec Revision 06](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-06.html) + - `encrypt`: Add support for `--profile=` option + - Add profile `rfc4880` to reflect status quo + - `version`: Add support for `--sop-spec` option + +## 1.4.6 +- Bump `sop-java` to `4.1.2` +- Fix `decrypt --verify-with` to not throw `NoSignature` exception (exit code 3) if `VERIFICAIONS` is empty. + +## 1.4.5 +- Bugfix: Direct-Key signatures are calculated over the signee key only, not the signer key + signee key +- Security: Fix faulty bit-strength policy check for signing subkeys + +## 1.4.4 +- Fix expectations on subpackets of v3 signatures (thanks @bjansen) + - Properly verify v3 signatures, which do not yet have signature subpackets, yet we required them to have + a hashed creation date subpacket. + +## 1.4.3 +- Bump `sop-java` to `4.1.1` +- Reuse shared test suite of `sop-java` +- Add `EncryptionOptions.hasEncryptionMethod()` +- SOP `encrypt`: Throw `MissingArg` exception if no encryption method was provided +- Fix redundant dot in exception message (thanks @DenBond7) + +## 1.4.2 +- Properly decrypt messages without MDC packets when `ConsumerOptions.setIgnoreMDCErrors(true)` is set +- Fix crash in `sop generate-key --with-key-password` when more than one user-id is given +- Revert integration with `pgp-certificate-store` +- Bump `sop-java` to `4.1.0` + +## 1.4.1 +- Add `UserId.parse()` method to parse user-ids into their components + +## 1.4.0 +- `sop generate-key`: Add support for keys without user-ids +- `sop inline-sign --as=clearsigned`: Make signature in TEXT mode +- Make countermeasures against [KOpenPGP](https://kopenpgp.com/) attacks configurable + - Countermeasures are now disabled by default since they are costly and have a specific threat model + - Can be enabled by calling `Policy.setEnableKeyParameterValidation(true)` + +## 1.4.0-rc2 +- Bump `bcpg-jdk15to18` to `1.72.3` +- Use BCs `PGPEncryptedDataList.extractSessionKeyEncryptedData()` method + to do decryption using session keys. This enables decryption of messages + without encrypted session key packets. +- Use BCs `PGPEncryptedDataList.isIntegrityProtected()` to check for integrity protection +- Depend on `pgp-certificate-store` +- Add `ConsumerOptions.addVerificationCerts(PGPCertificateStore)` to allow sourcing certificates from + e.g. a [certificate store implementation](https://github.com/pgpainless/cert-d-java). +- Make `DecryptionStream.getMetadata()` first class + - Deprecate `DecryptionStream.getResult()` + +## 1.4.0-rc1 +- Reimplement message consumption via new `OpenPgpMessageInputStream` + - Fix validation of prepended signatures (#314) + - Fix validation of nested signatures (#319) + - Reject malformed messages (#237) + - Utilize new `PDA` syntax verifier class + - Allow for custom message syntax via `Syntax` class + - Gracefully handle `UnsupportedPacketVersionException` for signatures + - Allow plugin decryption code (e.g. to add support for hardware-backed keys (see #318)) + - Add `HardwareSecurity` utility class + - Add `GnuPGDummyKeyUtil` which can be used to mimic GnuPGs proprietary S2K extensions + for keys which were placed on hardware tokens + - Add `OpenPgpPacket` enum class to enumerate available packet tags + - Remove old decryption classes in favor of new implementation + - Removed `DecryptionStream` class and replaced with new abstract class + - Removed `DecryptionStreamFactory` + - Removed `FinalIOException` + - Removed `MissingLiteralDataException` (replaced by `MalformedOpenPgpMessageException`) + - Introduce `MessageMetadata` class as potential future replacement for `OpenPgpMetadata`. + - can be obtained via `((OpenPgpMessageInputStream) decryptionStream).getMetadata();` +- Add `CachingBcPublicKeyDataDecryptorFactory` which can be extended to prevent costly decryption + of session keys +- Fix: Only verify message integrity once +- Remove unnecessary `@throws` declarations on `KeyRingReader` methods +- Remove unnecessary `@throws` declarations on `KeyRingUtils` methods +- Add `KeyIdUtil.formatKeyId(long id)` to format hexadecimal key-ids. +- Add `KeyRingUtils.publicKeys(PGPKeyRing keys)` +- Remove `BCUtil` class + +## 1.3.18 +- Bump `sop-java` to `4.1.2` +- Fix `decrypt --verify-with XYZ` not throwing `NoSignature` exception (exit code 3) if `VERIFICATIONS` is empty (#415) + +## 1.3.17 +- Bugfix: Direct-Key signatures are calculated over the signee key only, not the signer key + signee key +- Security: Fix faulty bit-strength policy check for signing subkeys + +## 1.3.16 +- Bump `sop-java` to `4.1.0` +- Bump `gradlew` to `7.5` + +## 1.3.15 +- Fix crash in `sop generate-key --with-key-password` when more than one user-id is given +- `sop generate-key`: Allow key generation without user-ids +- `sop inline-sign --as=clearsigned`: Make signatures of type 'text' instead of 'binary' + +## 1.3.14 +- Bump `bcpg` to `1.72.3` +- Fix DSA key parameter check +- Use proper method to unlock private signing keys when creating detached signatures + +## 1.3.13 +- Bump `sop-java` to `4.0.7` + +## 1.3.12 +- Bump `sop-java` to `4.0.5` +- Fix: `sop inline-sign`: Adopt `--as=clearsigned` instead of `--as=cleartextsigned` +- SOP: Hide `Version: PGPainless` armor header in all armored outputs +- Fix: `sop armor`: Do not re-armor already armored data + +## 1.3.11 +- Fix: When verifying subkey binding signatures with embedded recycled primary + key binding signatures, do not reject signature if primary key binding + predates subkey binding +- SOP `verify`: Forcefully expect `data()` to be non-OpenPGP data +- SOP `sign`: Fix matching of keys and passphrases +- CLI: Added tons of tests \o/ + +## 1.3.10 +- Bump `sop-java` to `4.0.3` +- Fix: Fix NPE when verifying signature made by key without key flags on direct-key signature + +## 1.3.9 +- Bump `sop-java` to `4.0.2` +- SOP: Improve exception handling + +## 1.3.8 +- Bump `bcprov` to `1.72` +- Bump `bcpg` to `1.72.1` +- Add `ProducerOptions.setHideArmorHeaders(boolean)` to hide automatically added armor headers + in encrypted messages + +## 1.3.7 +- Bugfix: Fix signature verification when `DecryptionStream` is drained byte-by-byte using `read()` call +- Add `KeyRingUtils.injectCertification(keys, certification)` +- Add `PGPainless.asciiArmor(key, outputStream)` +- Add `PGPainless.asciiArmor(signature)` + +## 1.3.6 +- Remove deprecated methods + - `ArmorUtils.createArmoredOutputStreamFor()` -> use `ArmorUtils.toAsciiArmoredStream()` instead + - `EncryptionResult.getSymmetricKeyAlgorithm()` -> use `EncryptionResult.getEncryptionAlgorithm()` instead +- Add `KeyRingInfo.getRevocationState()` + - Better way to determine whether a key is revoked +- Add `SigningOptions.addDetachedSignature(protector, key)` shortcut method +- Add `EncryptionOptions.get()`, `ConsumerOptions.get()` factory methods +- Add support for generating keys without user-id (only using `PGPainless.buildKeyRing()` for now) +- Switch to `SHA256` as default `S2K` hash algorithm for secret key protection +- Allow to set custom reference time when modifying secret keys +- Add diagnostic test to explore system PRNG performance + +## 1.3.5 +- Add `KeyRingInfo.isCapableOfSigning()` +- Add `KeyRingReader.readKeyRing(*)` methods that can take both secret- and public keys +- Add manpages + - Add script to generate manpages from sop-java-picocli +- Build website from main branch + +## 1.3.4 +- Fix `KeyRingInfo.isUsableForEncryption()`, `KeyRingInfo.isUsableForSigning()` not detecting revoked primary keys +- Bump `sop-java` and `sop-java-picocli` to `4.0.1` + - Fixes help text strings being resolved properly while allowing to override executable name + +## 1.3.3 +- Improve test compatibility against older JUnit versions +- Fix tests that read from jar-embedded resources (thanks @jcharaoui) +- `pgpainless-cli help`: Fix i18n strings + +## 1.3.2 +- Add `KeyRingInfo(Policy)` constructor +- Delete unused `KeyRingValidator` class +- Add `PGPainless.certify()` API + - `certify().userIdOnCertificate()` can be used to certify other users User-IDs + - `certify().certificate()` can be used to create direct-key signatures on other users keys +- We now have a [User Guide!](https://pgpainless.rtfd.io/) +- Fixed build script + - `pgpainless-cli`s `gradle build` task no longer builds fat jar + - Fat jars are now built by dedicated shadow plugin +- Fix third-party assigned user-ids on keys to accidentally get picked up as primary user-id +- Add `KeyRingUtils.publicKeyRingCollectionFrom(PGPSecretKeyRingCollection)` +- Add `SecretKeyRingEditor.replaceUserId(oldUid, newUid, protector)` +- Prevent adding `SymmetricKeyAlgorithm.NULL` (unencrypted) as encryption algo preference when generating keys + +## 1.3.1 +- Fix reproducibility of builds by setting fixed file permissions in archive task +- Improve encryption performance by buffering streams +- Fix `OpenPgpMetadata.isEncrypted()` to also return true for symmetrically encrypted messages +- SOP changes + - decrypt: Do not throw `NoSignatures` if no signatures found + - decrypt: Throw `BadData` when ciphertext is not encrypted + +## 1.3.0 +- Add `RevokedKeyException` +- `KeyRingUtils.stripSecretKey()`: Disallow stripping of primary secret key +- Remove support for reading compressed detached signatures +- Add `PGPainless.generateKeyRing().modernKeyRing(userId)` shortcut method without passphrase +- Add `CollectionUtils.addAll(Iterator, Collection)` +- Add `SignatureUtils.getSignaturesForUserIdBy(key, userId, keyId)` +- Add `OpenPgpFingerprint.parseFromBinary(bytes)` +- `SignatureUtils.wasIssuedBy()`: Add support for V5 fingerprints +- Prevent integer overflows when setting expiration dates +- SOP: Properly throw `KeyCannotDecrypt` exception +- Fix performance issues of encrypt and sign operations by using buffering +- Fix performance issues of armor and dearmor operations +- Bump dependency `sop-java` to `4.0.0` +- Add support for SOP specification version 04 + - Implement `inline-sign` + - Implement `inline-verify` + - Rename `DetachInbandSignatureAndMessageImpl` to `InlineDetachImpl` + - Rename `SignImpl` to `DetachedSignImpl` + - Rename `VerifyImpl` to `DetachedVerifyImpl` + - Add support for `--with-key-password` option in `GenerateKeyImpl`, `DetachedSignImpl`, `DecryptImpl`, `EncryptImpl`. + - `InlineDetachImpl` now supports 3 different message types: + - Messages using Cleartext Signature Framework + - OpenPGP messages using OnePassSignatures + - OpenPGP messages without OnePassSignatures +- Introduce `OpenPgpMetadata.isCleartextSigned()` + +## 1.2.2 +- `EncryptionOptions.addRecipients(collection)`: Disallow empty collections to prevent misuse from resulting in unencrypted messages +- Deprecate default policy factory methods in favor of policy factory methods with expressive names +- Another fix for OpenPGP data detection + - We now inspect the first packet of the data stream to figure out, whether it is plausible OpenPGP data, without exhausting the stream + +## 1.2.1 +- Bump `sop-java` dependency to `1.2.3` +- Bump `slf4j` dependency to `1.7.36` +- Bump `logback` dependency to `1.2.11` +- Add experimental support for creating signatures over pre-calculated `MessageDigest` objects. + - `BcHashContextSigner.signHashContext()` can be used to create OpenPGP signatures over manually hashed data. + This allows applications to do the hashing themselves. +- Harden detection of binary/ascii armored/non-OpenPGP data +- Add `ConsumerOptions.forceNonOpenPgpData()` to force PGPainless to handle data as non-OpenPGP data + - This is a workaround for when PGPainless accidentally mistakes non-OpenPGP data for binary OpenPGP data +- Implement "smart" hash algorithm policies, which take the 'usage-date' for algorithms into account + - This allows for fine-grained signature hash algorithm policing with usage termination dates +- Switch to smart signature hash algorithm policies by default + - PGPainless now accepts SHA-1 signatures if they were made before 2013-02-01 + - We also now accept RIPEMD160 signatures if they were made before 2013-02-01 + - We further accept MD5 signatures made prior to 1997-02-01 + + +## 1.2.0 +- Improve exception hierarchy for key-related exceptions + - See [PR](https://github.com/pgpainless/pgpainless/pull/261) for more information on how to migrate. +- Bump Bouncy Castle to `1.71` + - Switch from `bcpg-jdk15on:1.70` to `bcpg-jdk15to18:1.71` + - Switch from `bcprov-jdk15on:1.70` to `bcprov-jdk15to18:1.71` +- Implement merging of certificate copies + - can be used to implement updating certificates from key servers +- Fix `KeyRingUtils.keysPlusPublicKey()` +- Add support for adding `PolicyURI` and `RegularExpression` signature subpackets on signatures + +## 1.1.5 +- SOP encrypt: match signature type when using `encrypt --as=` option +- `ProducerOptions.setEncoding()`: The encoding is henceforth only considered metadata and will no longer trigger CRLF encoding. + - This fixes broken signature generation for mismatching (`StreamEncoding`,`DocumentSignatureType`) tuples. + - Applications that rely on CRLF-encoding can request PGPainless to apply this encoding by calling `ProducerOptions.applyCRLFEncoding(true)`. +- Rename `KeyRingUtils.removeSecretKey()` to `stripSecretKey()`. +- Add handy `SignatureOptions.addSignature()` method. +- Fix `ClassCastException` when evaluating a certificate with third party signatures. Thanks @p-barabas for the initial report and bug fix! + +## 1.1.4 +- Add utility method `KeyRingUtils.removeSecretKey()` to remove secret key part from key ring + - This can come in handy when using primary keys stored offline +- Add `EncryptionResult.isEncryptedFor(certificate)` +- `ArmorUtils.toAsciiArmoredString()` methods now print out primary user-id and brief information about further user-ids (thanks @bratkartoffel for the patch) +- Methods of `KeyRingUtils` and `ArmorUtils` classes are now annotated with `@Nonnull/@Nullable` +- Enums `fromId(code)` methods are now annotated with `@Nullable` and there are now `requireFromId(code)` counterparts which are `@Nonnull`. +- `ProducerOptions.setForYourEyesOnly()` is now deprecated (reason is deprecation in the +- [crypto-refresh-05](https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-05.html#name-special-filename-_console-d) document) +- Add `SessionKey.toString()` +- Partially fix generation of malformed signature packets when using different combinations of `StreamEncoding` and `DocumentSignatureType` values + - Unfortunately PGPainless still produces broken signatures when using either `StreamEncoding.TEXT` or `StreamEncoding.UTF8` in combination with `DocumentSignatureType.BINARY_DOCUMENT`. +- Deprecate `ProducerOptions.setEncoding(StreamEncoding)` + - Will be removed in a future release +- Remove `StreamEncoding.MIME` (was removed from the standard) + +## 1.1.3 +- Make `SigningOptions.getSigningMethods()` part of internal API +- Fix crash when trying to do verification of unmatched `SignersUserId` signature subpacket + - For now, verification of `SignersUserId` is disabled but can be enabled via `Policy.setSignerUserIdValidationLevel()` +- Initial support for `OpenPgpV5Fingerprint` +- Add `OpenPgpFingerprint.parse(string)` +- Security: Fix `KeyRingInfo.getValidAndExpiredUserIds()` accidentally including unbound user-ids + +## 1.0.5 +- Security: Fix `KeyRingInfo.getValidAndExpiredUserIds()` accidentally including unbound user-ids + +## 1.1.2 +- Fix `keyRingInfo.getEmailAddresses()` incorrectly matching some mail addresses (thanks @bratkartoffel for reporting and initial patch proposal) +- Fix generic type of `CertificationSubpackets.Callback` +- Add `KeyRingInfo.isUsableForEncryption()` +- Add `PGPainless.inspectKeyRing(key, date)` +- Allow custom key creation dates during key generation +- Reject subkeys with bindings that predate key generation +- `EncryptionOptions.addRecipient()`: Transform `NoSuchElementException` into `IllegalArgumentException` with proper error message +- Fix `ClassCastException` by preventing accidental verification of 3rd-party-issued user-id revocation with primary key. +- Fix `NullPointerException` when trying to verify malformed signature + +## 1.1.1 +- Add `producerOptions.setComment(string)` to allow adding ASCII armor comments when creating OpenPGP messages (thanks @ferenc-hechler) +- Simplify consumption of cleartext-signed data +- Change default criticality of signature subpackets + - Issuer Fingerprint: critical -> non-critical + - Revocable: non-critical -> critical + - Issuer KeyID: critical -> non-critical + - Preferred Algorithms: critical -> non-critical + - Revocation Reason: critical -> non-critical + +## 1.1.0 +- `pgpainless-sop`: Update `sop-java` to version 1.2.0 + - Treat passwords and session keys as indirect parameters + This means they are no longer treated as string input, but pointers to files or env variables + +## 1.0.4 +- Yet another patch for faulty ASCII armor detection 😒 + +## 1.0.3 +- Fix detection of unarmored data in signature verification + +## 1.0.2 +- Update SOP implementation to specification revision 03 +- Move `sop-java` and `sop-java-picocli` modules to [its own repository](https://github.com/pgpainless/sop-java) +- `OpenPGPV4Fingerprint`: Hex decode bytes in constructor +- Add `ArmorUtils.toAsciiArmoredString()` for single key +- Fix `ClassCastException` when retrieving `RevocationKey` subpackets from signatures +- Fix `pgpainless-sop` gradle script + - it now automatically pulls in transitive dependencies + +## 1.0.1 +- Fix sourcing of preferred algorithms by primary user-id when key is located via key-id + +## 1.0.0 +- Introduce `DateUtil.toSecondsPrecision()` +- Clean JUnit tests, fix code style issues and fix typos in documentation + +## 1.0.0-rc9 +- When key has both direct-key sig + primary user-id sig: resolve expiration date to the earliest expiration +- Add `SecretKeyRingEditor.removeUserId()` convenience methods that do soft-revoke the user-id. +- Add `SelectUserId.byEmail()` which also matches the plain email address + +## 1.0.0-rc8 +- `KeyRingInfo.getPrimaryUserId()`: return first user-id when no primary user-id is found +- Rename method `getBoundButPossiblyExpiredUserIds` to `getValidAndExpiredUserIds()` +- Remove audit resource material + +## 1.0.0-rc7 +- Make `Passphrase` comparison constant time +- Bump Bouncycastle to 1.70 + - Use new `PGPCanonicalizedDataGenerator` where applicable + - Implement decryption with user-provided session key + - Remove workaround for invalid signature processing +- Remove Blowfish from default symmetric decryption/encryption policy +- When adding/generating keys: Check compliance to `PublicKeyAlgorithmPolicy` +- Fix `BaseSecretKeyRingProtector` misinterpreting empty passphrases +- SOP: Fix NPE when attempting to sign with key with missing signing subkey +- Describe Threat Model in [pgpainless-core/README.md] +- Fix NPE when attempting to decrypt GNU_DUMMY_S2K key +- Validate public key parameters when unlocking secret keys +- Introduce iteration limits to prevent resource exhaustion when + - reading signatures + - reading keys +- `CachingSecretKeyRingProtector`: Prevent accidental passphrase overriding via `addPassphrase()` +- `EncryptionOptions`: replace method argument type `PGPPublicKeyRingCollection` with `Iterable` to allow for `Collection` as argument +- `SigningOptions`: replace method argument type `PGPSecretKeyRingCollection` with `Iterable` to allow for `Collection` as argument +- Prevent message decryption with non-encryption subkey +- Rework key modification API to fix inconsistency problems with expiration and primary user-ids. + - Remove methods to change expiration dates of subkeys and specific user-ids + - Rework primary user-id marking logic to unmark non-primary ids +- Added [Cure53 Security Audit Report](https://gh.pgpainless.org/assets/Audit-PGPainless.pdf) to the website +- Reworked tests for cryptographic backend to use custom `InvocationContextProvider` implementation +- Source `PGPObjectFactory` objects from `ImplementationProvider` +- Fix typo `getCommendHeader() -> getCommentHeader()` + +## 1.0.0-rc6 +- Restructure method arguments in `SecretKeyRingEditor` +- Add explanations of revocation reasons to `RevocationAttributes` +- Rename `CertificationSignatureBuilder` to `ThirdPartyCertificationSignatureBuilder` +- `KeyAccessor.ViaKeyId`: Differentiate between primary key (rely on direct-key sig) and subkey (subkey binding sig) +- Expose `SignatureSubpacketsUtil.getKeyLifetimeInSeconds` +- Various cleanup steps and new tests + +## 1.0.0-rc5 +- Fix invalid cursor mark in `BufferedInputStream` when processing large cleartext signed messages +- Add `SecretKeyRingEditor.revokeUserIds(SelectUserId, SecretKeyRingProtector, RevocationSignatureSubpackets.Callback)` + +## 1.0.0-rc4 +- Fix bug where `KeyRingBuilder` would mark additional user-ids as primary + +## 1.0.0-rc3 +- New Signature builder API for more fine-grained control over key-signatures: + - Introduce `CertificationSignatureSubpackets` builder class to wrap `PGPSignatureSubpacketGenerator` for + certification style signatures. + - Introduce `SelfSignatureSubpackets` builder class for self-signatures. + - Introduce `RevocationSignatureSubpackets` builder class for revocation signatures. + - Introduce `CertificationSignatureSubpackets.Callback`, `SelfSignatureSubpackets.Callback` and + `RevocationSignatureSubpackets.Callback` to allow modification of signature subpackets by the user. + - Incorporate `*SignatureSubpackets.Callback` classes as arguments in `SecretKeyRingEditor` and `KeyRingBuilder` methods. +- Start working on `ProofUtil` to create KeyOxide style identity proofs (WIP) +- Move Signature verification related code to `org.pgpainless.signature.consumer` package +- Ensure keyflags and other common subpackets are set in new signatures when adding user-ids +- Ensure subkey can carry keyflag when adding it to a key +- Refactor `SecretKeyRingProtector` methods and code + +## 1.0.0-rc2 +- `SecretKeyRingEditor`: Remove support for user-id- and subkey *deletion* in favor of *revocation* + - Deletion causes all sorts of problems. Most notably, receiving implementations will not honor deletion of user-ids/subkeys. + If you really need to delete user-ids there now is `KeyRingUtils.deleteUserId(keys, userid)`, + but its use is highly discouraged and should only (if ever) be used for local manipulations of keys. +- `pgpainless-core` & `pgpainless-sop`: Fix accidental compile scope dependency on `logback-classic` +- `KeyRingInfo`: Sensible arguments for methods to get preferred algorithms + +## 1.0.0-rc1 +- First release candidate for a 1.0.0 release! \o/ +- Rename `EncryptionPurpose.STORAGE_AND_COMMUNICATIONS` to `EncryptionPurpose.ANY` +- Hide `PGPainless.verifyCleartextSignedMessage()` behind `PGPainless.decryptAndVerify()`. + - the latter now checks whether the message is cleartext-signed or not and automatically calls the proper API + - `MultiPassStrategy` objects are now set through `ConsumerOptions.setMultiPassStrategy()`. +- Separate key ring generation through templates from custom key ring builder + - `PGPainless.generateKeyRing()` now offers to generate keys from templates + - `PGPainless.buildKeyRing()` offers a detailed API to build custom keys +- Fix detection of non-armored data + +## 0.2.19 +- Some preparations for OpenPGP V5 keys: `OpenPgpV4Fingerprint` is now an implementation of `OpenPgpFingerprint` +- `SignatureVerification` and `Failure` now have `toString()` implementations +- Logging: `logback-classic` is now an optional runtime dependency + +## 0.2.18 +- Fix compatibility with PGPainless < 0.2.10 +- Fix interoperability with Kleopatra + - Decryption: Do not skip over first PKESKs when we have a matching decryption key + - MessageInspector: Break from object factory loop after encountering encrypted data (we cannot go deeper) +- Move hash algorithm negotiation to own class +- Change return value of `EncryptionOptions.overrideEncryptionAlgorithm()` + +## 0.2.17 +- Fix prematurely throwing `MissingPassphraseException` when decrypting message with multiple possible keys and passphrases + ## 0.2.16 - Fix handling of subkey revocation signatures -- SOP: improve API use with byte arrays +- SOP: improve API usage with byte arrays - Fix `AssertionError` when determining encryption subkeys from set containing unbound key - Add `ConsumerOptions.setMissingKeyPassphraseStrategy(strategy)` to modify behavior when missing key passphrases are encountered during decryption diff --git a/CNAME b/CNAME new file mode 100644 index 00000000..b0cdad30 --- /dev/null +++ b/CNAME @@ -0,0 +1 @@ +gh.pgpainless.org \ No newline at end of file diff --git a/LICENSES/CC-BY-SA-3.0.txt b/LICENSES/CC-BY-SA-3.0.txt new file mode 100644 index 00000000..39a8591c --- /dev/null +++ b/LICENSES/CC-BY-SA-3.0.txt @@ -0,0 +1,99 @@ +Creative Commons Attribution-ShareAlike 3.0 Unported + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE LEGAL SERVICES. DISTRIBUTION OF THIS LICENSE DOES NOT CREATE AN ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE INFORMATION PROVIDED, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM ITS USE. + +License + +THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS CREATIVE COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS PROTECTED BY COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE WORK OTHER THAN AS AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS PROHIBITED. + +BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND AGREE TO BE BOUND BY THE TERMS OF THIS LICENSE. TO THE EXTENT THIS LICENSE MAY BE CONSIDERED TO BE A CONTRACT, THE LICENSOR GRANTS YOU THE RIGHTS CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH TERMS AND CONDITIONS. + +1. Definitions + + a. "Adaptation" means a work based upon the Work, or upon the Work and other pre-existing works, such as a translation, adaptation, derivative work, arrangement of music or other alterations of a literary or artistic work, or phonogram or performance and includes cinematographic adaptations or any other form in which the Work may be recast, transformed, or adapted including in any form recognizably derived from the original, except that a work that constitutes a Collection will not be considered an Adaptation for the purpose of this License. For the avoidance of doubt, where the Work is a musical work, performance or phonogram, the synchronization of the Work in timed-relation with a moving image ("synching") will be considered an Adaptation for the purpose of this License. + + b. "Collection" means a collection of literary or artistic works, such as encyclopedias and anthologies, or performances, phonograms or broadcasts, or other works or subject matter other than works listed in Section 1(f) below, which, by reason of the selection and arrangement of their contents, constitute intellectual creations, in which the Work is included in its entirety in unmodified form along with one or more other contributions, each constituting separate and independent works in themselves, which together are assembled into a collective whole. A work that constitutes a Collection will not be considered an Adaptation (as defined below) for the purposes of this License. + + c. "Creative Commons Compatible License" means a license that is listed at http://creativecommons.org/compatiblelicenses that has been approved by Creative Commons as being essentially equivalent to this License, including, at a minimum, because that license: (i) contains terms that have the same purpose, meaning and effect as the License Elements of this License; and, (ii) explicitly permits the relicensing of adaptations of works made available under that license under this License or a Creative Commons jurisdiction license with the same License Elements as this License. + + d. "Distribute" means to make available to the public the original and copies of the Work or Adaptation, as appropriate, through sale or other transfer of ownership. + + e. "License Elements" means the following high-level license attributes as selected by Licensor and indicated in the title of this License: Attribution, ShareAlike. + + f. "Licensor" means the individual, individuals, entity or entities that offer(s) the Work under the terms of this License. + + g. "Original Author" means, in the case of a literary or artistic work, the individual, individuals, entity or entities who created the Work or if no individual or entity can be identified, the publisher; and in addition (i) in the case of a performance the actors, singers, musicians, dancers, and other persons who act, sing, deliver, declaim, play in, interpret or otherwise perform literary or artistic works or expressions of folklore; (ii) in the case of a phonogram the producer being the person or legal entity who first fixes the sounds of a performance or other sounds; and, (iii) in the case of broadcasts, the organization that transmits the broadcast. + + h. "Work" means the literary and/or artistic work offered under the terms of this License including without limitation any production in the literary, scientific and artistic domain, whatever may be the mode or form of its expression including digital form, such as a book, pamphlet and other writing; a lecture, address, sermon or other work of the same nature; a dramatic or dramatico-musical work; a choreographic work or entertainment in dumb show; a musical composition with or without words; a cinematographic work to which are assimilated works expressed by a process analogous to cinematography; a work of drawing, painting, architecture, sculpture, engraving or lithography; a photographic work to which are assimilated works expressed by a process analogous to photography; a work of applied art; an illustration, map, plan, sketch or three-dimensional work relative to geography, topography, architecture or science; a performance; a broadcast; a phonogram; a compilation of data to the extent it is protected as a copyrightable work; or a work performed by a variety or circus performer to the extent it is not otherwise considered a literary or artistic work. + + i. "You" means an individual or entity exercising rights under this License who has not previously violated the terms of this License with respect to the Work, or who has received express permission from the Licensor to exercise rights under this License despite a previous violation. + + j. "Publicly Perform" means to perform public recitations of the Work and to communicate to the public those public recitations, by any means or process, including by wire or wireless means or public digital performances; to make available to the public Works in such a way that members of the public may access these Works from a place and at a place individually chosen by them; to perform the Work to the public by any means or process and the communication to the public of the performances of the Work, including by public digital performance; to broadcast and rebroadcast the Work by any means including signs, sounds or images. + + k. "Reproduce" means to make copies of the Work by any means including without limitation by sound or visual recordings and the right of fixation and reproducing fixations of the Work, including storage of a protected performance or phonogram in digital form or other electronic medium. + +2. Fair Dealing Rights. Nothing in this License is intended to reduce, limit, or restrict any uses free from copyright or rights arising from limitations or exceptions that are provided for in connection with the copyright protection under copyright law or other applicable laws. + +3. License Grant. Subject to the terms and conditions of this License, Licensor hereby grants You a worldwide, royalty-free, non-exclusive, perpetual (for the duration of the applicable copyright) license to exercise the rights in the Work as stated below: + + a. to Reproduce the Work, to incorporate the Work into one or more Collections, and to Reproduce the Work as incorporated in the Collections; + + b. to create and Reproduce Adaptations provided that any such Adaptation, including any translation in any medium, takes reasonable steps to clearly label, demarcate or otherwise identify that changes were made to the original Work. For example, a translation could be marked "The original work was translated from English to Spanish," or a modification could indicate "The original work has been modified."; + + c. to Distribute and Publicly Perform the Work including as incorporated in Collections; and, + + d. to Distribute and Publicly Perform Adaptations. + + e. For the avoidance of doubt: + + i. Non-waivable Compulsory License Schemes. In those jurisdictions in which the right to collect royalties through any statutory or compulsory licensing scheme cannot be waived, the Licensor reserves the exclusive right to collect such royalties for any exercise by You of the rights granted under this License; + + ii. Waivable Compulsory License Schemes. In those jurisdictions in which the right to collect royalties through any statutory or compulsory licensing scheme can be waived, the Licensor waives the exclusive right to collect such royalties for any exercise by You of the rights granted under this License; and, + + iii. Voluntary License Schemes. The Licensor waives the right to collect royalties, whether individually or, in the event that the Licensor is a member of a collecting society that administers voluntary licensing schemes, via that society, from any exercise by You of the rights granted under this License. + +The above rights may be exercised in all media and formats whether now known or hereafter devised. The above rights include the right to make such modifications as are technically necessary to exercise the rights in other media and formats. Subject to Section 8(f), all rights not expressly granted by Licensor are hereby reserved. + +4. Restrictions. The license granted in Section 3 above is expressly made subject to and limited by the following restrictions: + + a. You may Distribute or Publicly Perform the Work only under the terms of this License. You must include a copy of, or the Uniform Resource Identifier (URI) for, this License with every copy of the Work You Distribute or Publicly Perform. You may not offer or impose any terms on the Work that restrict the terms of this License or the ability of the recipient of the Work to exercise the rights granted to that recipient under the terms of the License. You may not sublicense the Work. You must keep intact all notices that refer to this License and to the disclaimer of warranties with every copy of the Work You Distribute or Publicly Perform. When You Distribute or Publicly Perform the Work, You may not impose any effective technological measures on the Work that restrict the ability of a recipient of the Work from You to exercise the rights granted to that recipient under the terms of the License. This Section 4(a) applies to the Work as incorporated in a Collection, but this does not require the Collection apart from the Work itself to be made subject to the terms of this License. If You create a Collection, upon notice from any Licensor You must, to the extent practicable, remove from the Collection any credit as required by Section 4(c), as requested. If You create an Adaptation, upon notice from any Licensor You must, to the extent practicable, remove from the Adaptation any credit as required by Section 4(c), as requested. + + b. You may Distribute or Publicly Perform an Adaptation only under the terms of: (i) this License; (ii) a later version of this License with the same License Elements as this License; (iii) a Creative Commons jurisdiction license (either this or a later license version) that contains the same License Elements as this License (e.g., Attribution-ShareAlike 3.0 US)); (iv) a Creative Commons Compatible License. If you license the Adaptation under one of the licenses mentioned in (iv), you must comply with the terms of that license. If you license the Adaptation under the terms of any of the licenses mentioned in (i), (ii) or (iii) (the "Applicable License"), you must comply with the terms of the Applicable License generally and the following provisions: (I) You must include a copy of, or the URI for, the Applicable License with every copy of each Adaptation You Distribute or Publicly Perform; (II) You may not offer or impose any terms on the Adaptation that restrict the terms of the Applicable License or the ability of the recipient of the Adaptation to exercise the rights granted to that recipient under the terms of the Applicable License; (III) You must keep intact all notices that refer to the Applicable License and to the disclaimer of warranties with every copy of the Work as included in the Adaptation You Distribute or Publicly Perform; (IV) when You Distribute or Publicly Perform the Adaptation, You may not impose any effective technological measures on the Adaptation that restrict the ability of a recipient of the Adaptation from You to exercise the rights granted to that recipient under the terms of the Applicable License. This Section 4(b) applies to the Adaptation as incorporated in a Collection, but this does not require the Collection apart from the Adaptation itself to be made subject to the terms of the Applicable License. + + c. If You Distribute, or Publicly Perform the Work or any Adaptations or Collections, You must, unless a request has been made pursuant to Section 4(a), keep intact all copyright notices for the Work and provide, reasonable to the medium or means You are utilizing: (i) the name of the Original Author (or pseudonym, if applicable) if supplied, and/or if the Original Author and/or Licensor designate another party or parties (e.g., a sponsor institute, publishing entity, journal) for attribution ("Attribution Parties") in Licensor's copyright notice, terms of service or by other reasonable means, the name of such party or parties; (ii) the title of the Work if supplied; (iii) to the extent reasonably practicable, the URI, if any, that Licensor specifies to be associated with the Work, unless such URI does not refer to the copyright notice or licensing information for the Work; and (iv) , consistent with Ssection 3(b), in the case of an Adaptation, a credit identifying the use of the Work in the Adaptation (e.g., "French translation of the Work by Original Author," or "Screenplay based on original Work by Original Author"). The credit required by this Section 4(c) may be implemented in any reasonable manner; provided, however, that in the case of a Adaptation or Collection, at a minimum such credit will appear, if a credit for all contributing authors of the Adaptation or Collection appears, then as part of these credits and in a manner at least as prominent as the credits for the other contributing authors. For the avoidance of doubt, You may only use the credit required by this Section for the purpose of attribution in the manner set out above and, by exercising Your rights under this License, You may not implicitly or explicitly assert or imply any connection with, sponsorship or endorsement by the Original Author, Licensor and/or Attribution Parties, as appropriate, of You or Your use of the Work, without the separate, express prior written permission of the Original Author, Licensor and/or Attribution Parties. + + d. Except as otherwise agreed in writing by the Licensor or as may be otherwise permitted by applicable law, if You Reproduce, Distribute or Publicly Perform the Work either by itself or as part of any Adaptations or Collections, You must not distort, mutilate, modify or take other derogatory action in relation to the Work which would be prejudicial to the Original Author's honor or reputation. Licensor agrees that in those jurisdictions (e.g. Japan), in which any exercise of the right granted in Section 3(b) of this License (the right to make Adaptations) would be deemed to be a distortion, mutilation, modification or other derogatory action prejudicial to the Original Author's honor and reputation, the Licensor will waive or not assert, as appropriate, this Section, to the fullest extent permitted by the applicable national law, to enable You to reasonably exercise Your right under Section 3(b) of this License (right to make Adaptations) but not otherwise. + +5. Representations, Warranties and Disclaimer + +UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING, LICENSOR OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY KIND CONCERNING THE WORK, EXPRESS, IMPLIED, STATUTORY OR OTHERWISE, INCLUDING, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTIBILITY, FITNESS FOR A PARTICULAR PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OF ABSENCE OF ERRORS, WHETHER OR NOT DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF IMPLIED WARRANTIES, SO SUCH EXCLUSION MAY NOT APPLY TO YOU. + +6. Limitation on Liability. EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY FOR ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY DAMAGES ARISING OUT OF THIS LICENSE OR THE USE OF THE WORK, EVEN IF LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +7. Termination + + a. This License and the rights granted hereunder will terminate automatically upon any breach by You of the terms of this License. Individuals or entities who have received Adaptations or Collections from You under this License, however, will not have their licenses terminated provided such individuals or entities remain in full compliance with those licenses. Sections 1, 2, 5, 6, 7, and 8 will survive any termination of this License. + + b. Subject to the above terms and conditions, the license granted here is perpetual (for the duration of the applicable copyright in the Work). Notwithstanding the above, Licensor reserves the right to release the Work under different license terms or to stop distributing the Work at any time; provided, however that any such election will not serve to withdraw this License (or any other license that has been, or is required to be, granted under the terms of this License), and this License will continue in full force and effect unless terminated as stated above. + +8. Miscellaneous + + a. Each time You Distribute or Publicly Perform the Work or a Collection, the Licensor offers to the recipient a license to the Work on the same terms and conditions as the license granted to You under this License. + + b. Each time You Distribute or Publicly Perform an Adaptation, Licensor offers to the recipient a license to the original Work on the same terms and conditions as the license granted to You under this License. + + c. If any provision of this License is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this License, and without further action by the parties to this agreement, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable. + + d. No term or provision of this License shall be deemed waived and no breach consented to unless such waiver or consent shall be in writing and signed by the party to be charged with such waiver or consent. + + e. This License constitutes the entire agreement between the parties with respect to the Work licensed here. There are no understandings, agreements or representations with respect to the Work not specified here. Licensor shall not be bound by any additional provisions that may appear in any communication from You. This License may not be modified without the mutual written agreement of the Licensor and You. + + f. The rights granted under, and the subject matter referenced, in this License were drafted utilizing the terminology of the Berne Convention for the Protection of Literary and Artistic Works (as amended on September 28, 1979), the Rome Convention of 1961, the WIPO Copyright Treaty of 1996, the WIPO Performances and Phonograms Treaty of 1996 and the Universal Copyright Convention (as revised on July 24, 1971). These rights and subject matter take effect in the relevant jurisdiction in which the License terms are sought to be enforced according to the corresponding provisions of the implementation of those treaty provisions in the applicable national law. If the standard suite of rights granted under applicable copyright law includes additional rights not granted under this License, such additional rights are deemed to be included in the License; this License is not intended to restrict the license of any rights under applicable law. + +Creative Commons Notice + +Creative Commons is not a party to this License, and makes no warranty whatsoever in connection with the Work. Creative Commons will not be liable to You or any party on any legal theory for any damages whatsoever, including without limitation any general, special, incidental or consequential damages arising in connection to this license. Notwithstanding the foregoing two (2) sentences, if Creative Commons has expressly identified itself as the Licensor hereunder, it shall have all rights and obligations of Licensor. + +Except for the limited purpose of indicating to the public that the Work is licensed under the CCPL, Creative Commons does not authorize the use by either party of the trademark "Creative Commons" or any related trademark or logo of Creative Commons without the prior written consent of Creative Commons. Any permitted use will be in compliance with Creative Commons' then-current trademark usage guidelines, as may be published on its website or otherwise made available upon request from time to time. For the avoidance of doubt, this trademark restriction does not form part of the License. + +Creative Commons may be contacted at http://creativecommons.org/. diff --git a/README.md b/README.md index d66357fc..e305e43a 100644 --- a/README.md +++ b/README.md @@ -6,28 +6,33 @@ SPDX-License-Identifier: Apache-2.0 # PGPainless - Use OpenPGP Painlessly! -[![Travis (.com)](https://travis-ci.com/pgpainless/pgpainless.svg?branch=master)](https://travis-ci.com/pgpainless/pgpainless) -[![Maven Central](https://badgen.net/maven/v/maven-central/org.pgpainless/pgpainless-core)](https://search.maven.org/artifact/org.pgpainless/pgpainless-core) -[![Coverage Status](https://coveralls.io/repos/github/pgpainless/pgpainless/badge.svg?branch=master)](https://coveralls.io/github/pgpainless/pgpainless?branch=master) -[![JavaDoc](https://badgen.net/badge/javadoc/yes/green)](https://pgpainless.org/releases/latest/javadoc/) -[![Interoperability Test-Suite](https://badgen.net/badge/Sequoia%20Test%20Suite/%232/green)](https://tests.sequoia-pgp.org/) +[![Build Status](https://github.com/pgpainless/pgpainless/actions/workflows/gradle_push.yml/badge.svg)](https://github.com/pgpainless/pgpainless/actions/workflows/gradle_push.yml) +[![Coverage Status](https://coveralls.io/repos/github/pgpainless/pgpainless/badge.svg?branch=main)](https://coveralls.io/github/pgpainless/pgpainless?branch=main) +[![Interoperability Test-Suite](https://badgen.net/badge/Sequoia%20Test%20Suite/results/green)](https://tests.sequoia-pgp.org/) [![PGP](https://img.shields.io/badge/pgp-A027%20DB2F%203E1E%20118A-blue)](https://keyoxide.org/7F9116FEA90A5983936C7CFAA027DB2F3E1E118A) [![REUSE status](https://api.reuse.software/badge/github.com/pgpainless/pgpainless)](https://api.reuse.software/info/github.com/pgpainless/pgpainless) +[![Documentation Status](https://readthedocs.org/projects/pgpainless/badge/?version=latest)](https://pgpainless.readthedocs.io/en/latest/?badge=latest) + +**PGPainless is an easy-to-use OpenPGP library for Java and Android applications** + +[![Packaging status](https://repology.org/badge/vertical-allrepos/pgpainless.svg)](https://repology.org/project/pgpainless/versions) +[![Maven Central](https://badgen.net/maven/v/maven-central/org.pgpainless/pgpainless-core)](https://search.maven.org/artifact/org.pgpainless/pgpainless-core) + ## About PGPainless aims to make using OpenPGP in Java projects as simple as possible. It does so by introducing an intuitive Builder structure, which allows easy setup of encryption/decryption operations, as well as straight forward key generation. -PGPainless is based around the Bouncycastle java library and can be used on Android down to API level 10. -It can be configured to either use the Java Cryptographic Engine (JCE), or Bouncycastles lightweight reimplementation. +PGPainless is based around the Bouncy Castle java library and can be used on Android down to API level 10. +It can be configured to either use the Java Cryptographic Engine (JCE), or Bouncy Castles lightweight reimplementation. -While signature verification in Bouncycastle is limited to signature correctness, PGPainless goes much further. +While signature verification in Bouncy Castle is limited to signature correctness, PGPainless goes much further. It also checks if signing subkeys are properly bound to their primary key, if keys are expired or revoked, as well as if keys are allowed to create signatures in the first place. These rigorous checks make PGPainless stand out from other Java-based OpenPGP libraries and are the reason why -PGPainless currently [*scores second place* on Sequoia-PGPs Interoperability Test-Suite](https://tests.sequoia-pgp.org). +PGPainless currently [*scores first place* on Sequoia-PGPs Interoperability Test-Suite](https://tests.sequoia-pgp.org). > At FlowCrypt we are using PGPainless in our Kotlin code bases on Android and on server side. > The ergonomics of legacy PGP tooling on Java is not very good, and PGPainless improves it greatly. @@ -35,12 +40,25 @@ PGPainless currently [*scores second place* on Sequoia-PGPs Interoperability Tes > > -Tom @ FlowCrypt.com +> Finally, testing irrefutably confirmed that the library removes many associated difficulties with PGP use in its provision of an approachable and uncomplicated API. +> In this regard, Paul Schaub deserves the utmost praise. +> +> -Mario @ Cure53.de + +## Get Started + +The very easiest way to start using OpenPGP on Java/Kotlin based systems is to use an implementation of [sop-java](https://github.com/pgpainless/sop-java). +`sop-java` defines a very stripped down API and is super easy to get started with. +Luckily PGPainless provides an implementation for the `sop-java` interface definitions in the form of [pgpainless-sop](pgpainless-sop/README.md). + +If you need more flexibility, directly using `pgpainless-core` is the way to go. + ## Features Most of PGPainless' features can be accessed directly from the `PGPainless` class. If you want to get started, this class is your friend :) -For further details you should check out the [javadoc](https://pgpainless.org/releases/latest/javadoc/)! +For further details you should check out the [javadoc](https://javadoc.io/doc/org.pgpainless/pgpainless-core)! ### Handle Keys Reading keys from ASCII armored strings or from binary files is easy: @@ -81,7 +99,7 @@ There are some predefined key archetypes, but it is possible to fully customize .modernKeyRing("Romeo ", "I defy you, stars!"); // Customized key - PGPSecretKeyRing keyRing = PGPainless.generateKeyRing() + PGPSecretKeyRing keyRing = PGPainless.buildKeyRing() .setPrimaryKey(KeySpec.getBuilder( RSA.withLength(RsaLength._8192), KeyFlag.SIGN_DATA, KeyFlag.CERTIFY_OTHER)) @@ -94,7 +112,7 @@ There are some predefined key archetypes, but it is possible to fully customize KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE) ).addUserId("Juliet ") .addUserId("xmpp:juliet@capulet.lit") - .setPassphrase("romeo_oh_Romeo<3") + .setPassphrase(Passphrase.fromPassword("romeo_oh_Romeo<3")) .build(); ``` @@ -114,7 +132,7 @@ Still it allows you to manually specify which algorithms to use of course. .addRecipient(aliceKey) .addRecipient(bobsKey) // optionally encrypt to a passphrase - .addPassphrase(Passphrase.fromPassword("password123")) + .addMessagePassphrase(Passphrase.fromPassword("password123")) // optionally override symmetric encryption algorithm .overrideEncryptionAlgorithm(SymmetricKeyAlgorithm.AES_192), new SigningOptions() @@ -129,6 +147,9 @@ Still it allows you to manually specify which algorithms to use of course. Streams.pipeAll(plaintextInputStream, encryptionStream); encryptionStream.close(); + + // Information about the encryption (algorithms, detached signatures etc.) + EncryptionResult result = encryptionStream.getResult(); ``` ### Decrypt and Verify Signatures @@ -151,10 +172,10 @@ This behaviour can be modified though using the `Policy` class. decryptionStream.close(); // Result contains information like signature status etc. - OpenPgpMetadata metadata = decryptionStream.getResult(); + MessageMetadata metadata = decryptionStream.getMetadata(); ``` -*After* the `DecryptionStream` was closed, you can get metadata about the processed data by retrieving the `OpenPgpMetadata`. +*After* the `DecryptionStream` was closed, you can get metadata about the processed data by retrieving the `MessageMetadata`. Again, this object will contain information about how the message was encrypted, who signed it and so on. #### Many more examples can be found in the [examples package](pgpainless-core/src/test/java/org/pgpainless/example)!!! @@ -170,30 +191,38 @@ repositories { } dependencies { - implementation 'org.pgpainless:pgpainless-core:0.2.16' + implementation 'org.pgpainless:pgpainless-core:1.7.6' } ``` -## About -PGPainless is a by-product of my [Summer of Code 2018 project](https://blog.jabberhead.tk/summer-of-code-2018/). -For that project I was in need of a simple to use OpenPGP library. - -Originally I was going to use [Bouncy-GPG](https://github.com/neuhalje/bouncy-gpg) for my project, -but ultimately I decided to create my own OpenPGP library which better fits my needs. - -However, PGPainless is heavily influenced by Bouncy-GPG. - -To reach out to the development team, feel free to send a mail: info@pgpainless.org +## Professional Support +Do you need a custom feature? Are you unsure of what's the best way to integrate PGPainless into your product? +We offer paid professional services. Don't hesitate to send an inquiry to [info@pgpainless.org](mailto:info@pgpainless.org). ## Development +Join the projects IRC channel [**#pgpainless**](ircs://irc.oftc.net:6697/#pgpainless) on OFTC if you have any questions! + PGPainless is developed in - and accepts contributions from - the following places: * [Github](https://github.com/pgpainless/pgpainless) * [Codeberg](https://codeberg.org/PGPainless/pgpainless) +We are using SemVer (MAJOR.MINOR.PATCH) versioning, although MINOR releases could contain breaking changes from time to time. + +If you want to contribute a bug fix, please check the `release/X.Y` branches first to see, what the oldest release is +which contains the bug you are fixing. That way we can update older revisions of the library easily. + Please follow the [code of conduct](CODE_OF_CONDUCT.md) if you want to be part of the project. ## Acknowledgements Development on PGPainless is generously sponsored by [FlowCrypt.com](https://flowcrypt.com). Thank you very very very much! +[![FlowCrypt Logo](https://blog.jabberhead.tk/wp-content/uploads/2022/05/flowcrypt-logo.svg)](https://flowcrypt.com) -Continuous Integration is kindly provided by [Travis-CI.com](https://travis-ci.com/). +Parts of PGPainless development ([project page](https://nlnet.nl/project/PGPainless/)) will be funded by [NGI Assure](https://nlnet.nl/assure/) through [NLNet](https://nlnet.nl). +NGI Assure is made possible with financial support from the [European Commission](https://ec.europa.eu/)'s [Next Generation Internet](https://ngi.eu/) programme, under the aegis of [DG Communications Networks, Content and Technology](https://ec.europa.eu/info/departments/communications-networks-content-and-technology_en). +[![NGI Assure Logo](https://blog.jabberhead.tk/wp-content/uploads/2022/05/NGIAssure_tag.svg)](https://nlnet.nl/assure/) + +Big thank you also to those who decided to support the work by donating! +Notably @msfjarvis + +You make my day! diff --git a/REUSE.toml b/REUSE.toml new file mode 100644 index 00000000..66b5e867 --- /dev/null +++ b/REUSE.toml @@ -0,0 +1,118 @@ +version = 1 +SPDX-PackageName = "PGPainless" +SPDX-PackageSupplier = "Paul Schaub " +SPDX-PackageDownloadLocation = "https://pgpainless.org" + +[[annotations]] +path = "REUSE.toml" +precedence = "aggregate" +SPDX-FileCopyrightText = "2025 Paul Schaub " +SPDX-License-Identifier = "CC0-1.0" + +[[annotations]] +path = ".git-blame-ignore-revs" +precedence = "aggregate" +SPDX-FileCopyrightText = "2023 Paul Schaub " +SPDX-License-Identifier = "CC0-1.0" + +[[annotations]] +path = "docs/**" +precedence = "aggregate" +SPDX-FileCopyrightText = "2022 Paul Schaub " +SPDX-License-Identifier = "CC-BY-3.0" + +[[annotations]] +path = ".readthedocs.yaml" +precedence = "aggregate" +SPDX-FileCopyrightText = "2022 Paul Schaub " +SPDX-License-Identifier = "CC0-1.0" + +[[annotations]] +path = "gradle**" +precedence = "aggregate" +SPDX-FileCopyrightText = "2015 the original author or authors." +SPDX-License-Identifier = "Apache-2.0" + +[[annotations]] +path = ".editorconfig" +precedence = "aggregate" +SPDX-FileCopyrightText = "Facebook" +SPDX-License-Identifier = "Apache-2.0" + +[[annotations]] +path = "assets/repository-open-graph.png" +precedence = "aggregate" +SPDX-FileCopyrightText = "2021 Paul Schaub " +SPDX-License-Identifier = "CC-BY-3.0" + +[[annotations]] +path = "assets/pgpainless.svg" +precedence = "aggregate" +SPDX-FileCopyrightText = "2021 Paul Schaub " +SPDX-License-Identifier = "CC-BY-3.0" + +[[annotations]] +path = "assets/logo.png" +precedence = "aggregate" +SPDX-FileCopyrightText = "2022 Paul Schaub " +SPDX-License-Identifier = "CC-BY-3.0" + +[[annotations]] +path = "assets/test_vectors/**" +precedence = "aggregate" +SPDX-FileCopyrightText = "2018 Paul Schaub " +SPDX-License-Identifier = "CC0-1.0" + +[[annotations]] +path = "pgpainless-core/src/test/resources/**" +precedence = "aggregate" +SPDX-FileCopyrightText = "2020 Paul Schaub " +SPDX-License-Identifier = "CC0-1.0" + +[[annotations]] +path = "audit/**" +precedence = "aggregate" +SPDX-FileCopyrightText = "2021 Paul Schaub " +SPDX-License-Identifier = "CC0-1.0" + +[[annotations]] +path = "CNAME" +precedence = "aggregate" +SPDX-FileCopyrightText = "2022 Paul Schaub " +SPDX-License-Identifier = "CC0-1.0" + +[[annotations]] +path = "_config.yml" +precedence = "aggregate" +SPDX-FileCopyrightText = "2022 Paul Schaub " +SPDX-License-Identifier = "CC0-1.0" + +[[annotations]] +path = "_layouts/**" +precedence = "aggregate" +SPDX-FileCopyrightText = "2022 Paul Schaub , 2017 Steve Smith" +SPDX-License-Identifier = "CC-BY-SA-3.0" + +[[annotations]] +path = "pgpainless-cli/src/main/resources/META-INF/native-image/**" +precedence = "aggregate" +SPDX-FileCopyrightText = "2025 Paul Schaub " +SPDX-License-Identifier = "Apache-2.0" + +[[annotations]] +path = "pgpainless-cli/rewriteManPages.sh" +precedence = "aggregate" +SPDX-FileCopyrightText = "2022 Paul Schaub " +SPDX-License-Identifier = "Apache-2.0" + +[[annotations]] +path = "pgpainless-cli/packaging/man/**" +precedence = "aggregate" +SPDX-FileCopyrightText = "2022 Paul Schaub " +SPDX-License-Identifier = "Apache-2.0" + +[[annotations]] +path = ".github/ISSUE_TEMPLATE/**" +precedence = "aggregate" +SPDX-FileCopyrightText = "2024 Paul Schaub " +SPDX-License-Identifier = "CC0-1.0" diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..a05079d3 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,37 @@ + + + +# Security Policy + +## Supported Versions + +Use this section to tell people about which versions of your project are +currently being supported with security updates. + +| Version | Supported | Note | +|---------|--------------------|------------| +| 1.7.X | :white_check_mark: | | +| 1.6.X | :white_check_mark: | LTS branch | +| 1.5.X | :white_check_mark: | | +| 1.4.X | :white_check_mark: | | +| 1.3.X | :white_check_mark: | LTS branch | +| < 1.3.X | :x: | | + +## Reporting a Vulnerability + +If you find a security relevant vulnerability inside PGPainless, please let me know! +[Here](https://keyoxide.org/7F9116FEA90A5983936C7CFAA027DB2F3E1E118A) you can find my OpenPGP key to email me confidentially. + +Valid security issues will be fixed ASAP. + +## Audits + +### Cure53 - FLO-04 +PGPainless has received a security audit by [cure53.de](https://cure53.de) in late 2021. +The [penetrationj test and audit](https://cure53.de/pentest-report_pgpainless.pdf) covered PGPainless +release candidate 1.0.0-rc6. +Security fixes for discovered flaws were deployed before the final 1.0.0 release. \ No newline at end of file diff --git a/_config.yml b/_config.yml new file mode 100644 index 00000000..bdfcc3ab --- /dev/null +++ b/_config.yml @@ -0,0 +1,8 @@ +logo: /assets/logo.png +theme: jekyll-theme-minimal + +exclude: + - CHANGELOG.md + - CODE_OF_CONDUCT.md + - SECURITY.md + - docs diff --git a/_layouts/default.html b/_layouts/default.html new file mode 100644 index 00000000..879bb4a0 --- /dev/null +++ b/_layouts/default.html @@ -0,0 +1,76 @@ + + + + + + {{ site.title | default: site.github.repository_name }} by {{ +site.github.owner_name }} + + + + + + +
+
+ {% if site.logo %} + Logo + {% endif %} +

{{ site.description | default: site.github.project_tagline +}}

+ + Home +
+ Releases +
+ Documentation +
+ Javadoc +
+ Coverage +
+ + + {% if site.github.is_project_page %} +

View the Project on GitHub {{ github_name }}

+ {% endif %} + + {% if site.github.is_user_page %} +

View My +GitHub Profile

+ {% endif %} + + {% if site.show_downloads %} + + {% endif %} +
+
+ + {{ content }} + +
+ +
+ + + diff --git a/assets/logo.png b/assets/logo.png new file mode 100644 index 00000000..6cfae820 Binary files /dev/null and b/assets/logo.png differ diff --git a/assets/pgpainless.svg b/assets/pgpainless.svg new file mode 100644 index 00000000..47588bd8 --- /dev/null +++ b/assets/pgpainless.svg @@ -0,0 +1,110 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/repository-open-graph.png b/assets/repository-open-graph.png index 93ef34a1..4df928e5 100644 Binary files a/assets/repository-open-graph.png and b/assets/repository-open-graph.png differ diff --git a/audit/audit_cert.asc b/audit/audit_cert.asc deleted file mode 100644 index 6d02434f..00000000 --- a/audit/audit_cert.asc +++ /dev/null @@ -1,21 +0,0 @@ ------BEGIN PGP PUBLIC KEY BLOCK----- -Version: PGPainless -Comment: A16D 7A3E A645 F32B FFD2 9B30 DCDE B25E D526 4E4C -Comment: Audit - -mDMEYUhIQhYJKwYBBAHaRw8BAQdAz7UK1857JYHm+09xETHXsMYAyJWYir4SCVZc -FLfA/Eu0HEF1ZGl0IDxhdWRpdEBwZ3BhaW5sZXNzLm9yZz6IeAQTFgoAIAUCYUhI -QgIbAQUWAgMBAAQLCQgHBRUKCQgLAh4BAhkBAAoJENzesl7VJk5MqAABAJujTgJt -YmvyuGgeY6qiAOz5JTw8H2SShFtDbSNvKNt2AQDQDSauqTHxNh5DzkHxZv/+r/30 -Jhwj7cpW+Ktd83rvDLg4BGFISEISCisGAQQBl1UBBQEBB0AtCBfO8W27Ame+Zmar -nNeJxXfJXeVjfLMh37HjvzMaXgMBCAeIdQQYFgoAHQUCYUhIQgIbDAUWAgMBAAQL -CQgHBRUKCQgLAh4BAAoJENzesl7VJk5MbckBAMcDt/pRSltae0X6TC4nj43HBLXN -GEnv8vOOKzrwLbdiAQCJlDpWCC8GLfujEsaDmPCp59hQiGv4g2pY2zzEgicvArgz -BGFISEIWCSsGAQQB2kcPAQEHQHnZ1djBXKZXhnsz905kXomsyWNbNklfSNp0tJR5 -a/oNiNUEGBYKAH0FAmFISEICGwIFFgIDAQAECwkIBwUVCgkICwIeAV8gBBkWCgAG -BQJhSEhCAAoJEBPUPQ7ll5OWutIBAO1P/SIsaisKjmdaEPDn8x6hLikzPzjOJlZm -QYHBOCNXAP9wMQMInGDYAj1Sz67Z7Rjl6f4sOB/P6Tv9V4rbZwyNDAAKCRDc3rJe -1SZOTMgWAP9EBlU8v/Nj8rDo6ZT4RFAdVwV/YqOj1UgEe/paTFhPSgD/ezgwk4xF -UTvWjgYsHwtm94hgQfpu5P7ZdWhNMEBwHgg= -=/5mN ------END PGP PUBLIC KEY BLOCK----- diff --git a/audit/booby-traps/trap01.asc b/audit/booby-traps/trap01.asc deleted file mode 100644 index a57b55de..00000000 --- a/audit/booby-traps/trap01.asc +++ /dev/null @@ -1,43 +0,0 @@ ------BEGIN PGP MESSAGE----- -Version: PGPainless - -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 -=LTKS ------END PGP MESSAGE----- diff --git a/build.gradle b/build.gradle index cac39a9e..93100f4d 100644 --- a/build.gradle +++ b/build.gradle @@ -18,7 +18,8 @@ buildscript { } plugins { - id 'ru.vyarus.animalsniffer' version '1.5.3' + id 'org.jetbrains.kotlin.jvm' version "1.8.10" + id 'com.diffplug.spotless' version '6.22.0' apply false } apply from: 'version.gradle' @@ -29,44 +30,58 @@ allprojects { apply plugin: 'eclipse' apply plugin: 'jacoco' apply plugin: 'checkstyle' + apply plugin: 'kotlin' + apply plugin: 'com.diffplug.spotless' - // For non-sop modules, enable android api compatibility check - if (it.name.equals('pgpainless-core') || it.name.equals('sop-java') || it.name.equals('pgpainless-sop')) { - // animalsniffer - apply plugin: 'ru.vyarus.animalsniffer' - dependencies { - signature "net.sf.androidscents.signature:android-api-level-${pgpainlessMinAndroidSdk}:2.3.3_r2@signature" - } - animalsniffer { - sourceSets = [sourceSets.main] - } + // Only generate jar for submodules + // without this we would generate an empty pgpainless.jar for the project root + // https://stackoverflow.com/a/25445035 + jar { + reproducibleFileOrder = true + onlyIf { !sourceSets.main.allSource.files.isEmpty() } } // checkstyle checkstyle { - toolVersion = '8.18' + toolVersion = '10.25.0' + } + + spotless { + kotlin { + ktfmt().dropboxStyle() + } } group 'org.pgpainless' description = "Simple to use OpenPGP API for Java based on Bouncycastle" version = shortVersion - sourceCompatibility = javaSourceCompatibility - repositories { mavenCentral() + mavenLocal() } // Reproducible Builds tasks.withType(AbstractArchiveTask) { preserveFileTimestamps = false reproducibleFileOrder = true + + dirMode = 0755 + fileMode = 0644 + } + + kotlin { + jvmToolchain(javaSourceCompatibility) + } + + // Compatibility of default implementations in kotlin interfaces with Java implementations. + tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { + kotlinOptions { + freeCompilerArgs += ["-Xjvm-default=all-compatibility"] + } } project.ext { - slf4jVersion = '1.7.32' - junitVersion = '5.7.2' - picocliVersion = '4.6.1' rootConfigDir = new File(rootDir, 'config') gitCommit = getGitCommit() isContinuousIntegrationEnvironment = Boolean.parseBoolean(System.getenv('CI')) @@ -91,7 +106,7 @@ allprojects { } jacoco { - toolVersion = "0.8.7" + toolVersion = "0.8.8" } jacocoTestReport { @@ -99,7 +114,7 @@ allprojects { sourceDirectories.setFrom(project.files(sourceSets.main.allSource.srcDirs)) classDirectories.setFrom(project.files(sourceSets.main.output)) reports { - xml.enabled true + xml.required = true } } @@ -117,15 +132,15 @@ subprojects { apply plugin: 'signing' task sourcesJar(type: Jar, dependsOn: classes) { - classifier = 'sources' + archiveClassifier = 'sources' from sourceSets.main.allSource } task javadocJar(type: Jar, dependsOn: javadoc) { - classifier = 'javadoc' + archiveClassifier = 'javadoc' from javadoc.destinationDir } task testsJar(type: Jar, dependsOn: testClasses) { - classifier = 'tests' + archiveClassifier = 'tests' from sourceSets.test.output } @@ -222,7 +237,7 @@ task jacocoRootReport(type: JacocoReport) { classDirectories.setFrom(files(subprojects.sourceSets.main.output)) executionData.setFrom(files(subprojects.jacocoTestReport.executionData)) reports { - xml.enabled true + xml.required = true xml.destination file("${buildDir}/reports/jacoco/test/jacocoTestReport.xml") } // We could remove the following setOnlyIf line, but then @@ -233,10 +248,6 @@ task jacocoRootReport(type: JacocoReport) { } task javadocAll(type: Javadoc) { - def currentJavaVersion = JavaVersion.current() - if (currentJavaVersion.compareTo(JavaVersion.VERSION_1_9) >= 0) { - options.addStringOption("-release", "8"); - } source subprojects.collect {project -> project.sourceSets.main.allJava } destinationDir = new File(buildDir, 'javadoc') @@ -249,3 +260,23 @@ task javadocAll(type: Javadoc) { "https://docs.oracle.com/javase/${sourceCompatibility.getMajorVersion()}/docs/api/", ] as String[] } + +/** + * Fetch sha256 checksums of artifacts published to maven central. + * + * Example: gradle -Prelease=1.3.13 mavenCentralChecksums + */ +task mavenCentralChecksums() { + description 'Fetch and display checksums for artifacts published to Maven Central' + String ver = project.hasProperty('release') ? release : shortVersion + doLast { + for (Project p : rootProject.subprojects) { + String url = "https://repo1.maven.org/maven2/org/pgpainless/${p.name}/${ver}/${p.name}-${ver}.jar.sha256" + Process fetch = "curl -f $url".execute() + if (fetch.waitFor() == 0) { + print fetch.text.trim() + println " ${p.name}/build/libs/${p.name}-${ver}.jar" + } + } + } +} diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index 06e167fd..7a6cb4f5 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -114,18 +114,12 @@ SPDX-License-Identifier: CC0-1.0 - - - + - - - - diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 00000000..d0c3cbf1 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..f3e156ef --- /dev/null +++ b/docs/README.md @@ -0,0 +1,31 @@ +# User Guide for PGPainless + +Documentation for PGPainless is built from Markdown using Sphinx and MyST. + +A built version of the documentation is available on http://pgpainless.rtfd.io/ + +## Useful resources + +* [Sphix Documentation Generator](https://www.sphinx-doc.org/en/master/) +* [MyST Markdown Syntax](https://myst-parser.readthedocs.io/en/latest/index.html) + +## Build the Guide + +To build: + +```shell +$ make {html|epub|latexpdf} +``` + +Note: Diagrams are currently not built from source. +Instead, pre-built image files are used directly, because there are issues with mermaid in CLI systems. + +If you want to build the diagrams from source, you need `mermaid-cli` to be installed on your system. +```shell +$ npm install -g @mermaid-js/mermaid-cli +``` + +You can then use `mmdc` to build/update single diagram files like this: +```shell +mmdc --theme default --width 1600 --backgroundColor transparent -i ecosystem_dia.md -o ecosystem_dia.svg +``` diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 00000000..6247f7e2 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 00000000..ab1cf848 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,3 @@ +myst-parser>=0.17 +sphinxcontrib-mermaid>=0.7.1 +sphinx_rtd_theme>=2.0.0 diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 00000000..aeb057b3 --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,56 @@ +import os +# Configuration file for the Sphinx documentation builder. + +# -- Project information + +project = 'PGPainless' +copyright = '2022, Paul Schaub' +author = 'Paul Schaub' + +master_doc = 'index' + +# https://protips.readthedocs.io/git-tag-version.html +latest_tag = os.popen('git describe --abbrev=0').read().strip() +release = latest_tag +version = release + +myst_substitutions = { + "repo_host" : "codeberg.org", +# "repo_host" : "github.com", + "repo_pgpainless_src" : "codeberg.org/pgpainless/pgpainless/src/branch", +# "repo_pgpainless_src" : "github.com/pgpainless/pgpainless/tree", +} + +# -- General configuration + +extensions = [ + 'myst_parser', + 'sphinxcontrib.mermaid', + 'sphinx.ext.duration', + 'sphinx.ext.doctest', + 'sphinx.ext.autodoc', + 'sphinx.ext.autosummary', +] + +source_suffix = ['.rst', '.md'] + +myst_enable_extensions = [ + 'colon_fence', + 'substitution', +] + +myst_heading_anchors = 3 + +templates_path = ['_templates'] + +# -- Options for HTML output + +html_theme = 'sphinx_rtd_theme' + +# Show URLs as footnotes +#epub_show_urls = 'footnote' +latex_show_urls = 'footnote' + +# 'raw' does not work for epub and pdf, neither does 'svg' +mermaid_output_format = 'png' +mermaid_params = ['--theme', 'default', '--width', '1600', '--backgroundColor', 'transparent'] diff --git a/docs/source/ecosystem.md b/docs/source/ecosystem.md new file mode 100644 index 00000000..e5294b9b --- /dev/null +++ b/docs/source/ecosystem.md @@ -0,0 +1,59 @@ +# The PGPainless Ecosystem + +PGPainless consists of an ecosystem of different libraries and projects. + +The diagram below shows, how the different projects relate to one another. + +![Ecosystem](ecosystem_dia.*) + + +## Libraries and Tools + +* {{ '[PGPainless](https://{}/pgpainless/pgpainless)'.format(repo_host) }} + The main repository contains the following components: + * `pgpainless-core` - core implementation - powerful, yet easy to use OpenPGP API + * `pgpainless-sop` - super simple OpenPGP implementation. Drop-in for `sop-java` + * `pgpainless-cli` - SOP CLI implementation using PGPainless + +* {{ '[SOP-Java](https://{}/pgpainless/sop-java)'.format(repo_host) }} + An API definition and CLI implementation of the [Stateless OpenPGP Protocol](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/) (SOP). + Consumers of the SOP API can simply depend on `sop-java` and then switch out the backend as they wish. + Read more about the [SOP](sop) protocol here. + * `sop-java` - generic OpenPGP API definition + * `sop-java-picocli` - CLI frontend for `sop-java` + +* {{ '[WKD-Java](https://{}/pgpainless/wkd-java)'.format(repo_host) }} + Implementation of the [Web Key Directory](https://www.ietf.org/archive/id/draft-koch-openpgp-webkey-service-13.html). + * `wkd-java` - generic WKD discovery implementation + * `wkd-java-cli` - CLI frontend for WKD discovery using PGPainless + * `wkd-test-suite` - Generator for test vectors for testing WKD implementations + +* {{ '[VKS-Java](https://{}/pgpainless/vks-java)'.format(repo_host) }} + Client-side API for communicating with Verifying Key Servers, such as https://keys.openpgp.org/. + * `vks-java` - VKS client implementation + * `vks-java-cli` - CLI frontend for `vks-java` + +* {{ '[Cert-D-Java](https://{}/pgpainless/cert-d-java)'.format(repo_host) }} + Implementations of the [Shared OpenPGP Certificate Directory specification](https://sequoia-pgp.gitlab.io/pgp-cert-d/). + * `pgp-certificate-store` - abstract definitions of OpenPGP certificate stores + * `pgp-cert-d-java` - implementation of `pgp-certificate-store` following the PGP-CERT-D spec + * `pgp-cert-d-java-jdbc-sqlite-lookup` - subkey lookup using sqlite database + +* {{ '[Cert-D-PGPainless](https://{}/pgpainless/cert-d-pgpainless)'.format(repo_host) }} + Implementation of the [Shared OpenPGP Certificate Directory specification](https://sequoia-pgp.gitlab.io/pgp-cert-d/) using PGPainless. + * `pgpainless-cert-d` - PGPainless-based implementation of `pgp-cert-d-java` + * `pgpainless-cert-d-cli` - CLI frontend for `pgpainless-cert-d` + +* {{ '[PGPainless-WOT](https://{}/pgpainless/pgpainless-wot)'.format(repo_host) }} + Implementation of the [OpenPGP Web of Trust specification](https://sequoia-pgp.gitlab.io/sequoia-wot/) using PGPainless. + * `pgpainless-wot` - Parse OpenPGP keyrings into a generic `Network` object + * `wot-dijkstra` - Perform queries to find paths inside a `Network` object + * `pgpainless-wot-cli` - CLI frontend for `pgpainless-wot` and `wot-dijkstra` + * `wot-test-suite` - Test vectors ported from [Sequoia-PGPs WoT implementation](https://gitlab.com/sequoia-pgp/sequoia-wot/-/tree/main/tests/data) + +* {{ '[PGPeasy](https://{}/pgpainless/pgpeasy)'.format(repo_host) }} + Prototypical, comprehensive OpenPGP CLI application + * `pgpeasy` - CLI application \ No newline at end of file diff --git a/docs/source/ecosystem_dia.md b/docs/source/ecosystem_dia.md new file mode 100644 index 00000000..bc4bd190 --- /dev/null +++ b/docs/source/ecosystem_dia.md @@ -0,0 +1,45 @@ +```mermaid +flowchart LR + subgraph SOP-JAVA + sop-java-picocli-->sop-java + end + subgraph PGPAINLESS + pgpainless-sop-->pgpainless-core + pgpainless-sop-->sop-java + pgpainless-cli-->pgpainless-sop + pgpainless-cli-->sop-java-picocli + end + subgraph WKD-JAVA + wkd-java-cli-->wkd-java + wkd-test-suite-->wkd-java + wkd-test-suite-->pgpainless-core + end + subgraph CERT-D-JAVA + pgp-cert-d-java-->pgp-certificate-store + pgp-cert-d-java-jdbc-sqlite-lookup-->pgp-cert-d-java + end + subgraph CERT-D-PGPAINLESS + pgpainless-cert-d-->pgpainless-core + pgpainless-cert-d-->pgp-cert-d-java + pgpainless-cert-d-cli-->pgpainless-cert-d + pgpainless-cert-d-cli-->pgp-cert-d-java-jdbc-sqlite-lookup + end + subgraph VKS-JAVA + vks-java-cli-->vks-java + end + subgraph PGPAINLESS-WOT + wot-test-suite-->pgpainless-wot + pgpainless-wot-->wot-dijkstra + pgpainless-wot-cli-->pgpainless-wot + pgpainless-wot-->pgpainless-core + pgpainless-wot-cli-->pgpainless-cert-d + end + subgraph PGPEASY + pgpeasy-->pgpainless-cli + pgpeasy-->wkd-java-cli + pgpeasy-->vks-java-cli + pgpeasy-->pgpainless-cert-d-cli + end + wkd-java-cli-->pgpainless-cert-d + wkd-java-->pgp-certificate-store +``` \ No newline at end of file diff --git a/docs/source/ecosystem_dia.png b/docs/source/ecosystem_dia.png new file mode 100644 index 00000000..0efa16a8 Binary files /dev/null and b/docs/source/ecosystem_dia.png differ diff --git a/docs/source/ecosystem_dia.svg b/docs/source/ecosystem_dia.svg new file mode 100644 index 00000000..7ace9a54 --- /dev/null +++ b/docs/source/ecosystem_dia.svg @@ -0,0 +1 @@ +
PGPEASY
PGPAINLESS-WOT
VKS-JAVA
CERT-D-PGPAINLESS
CERT-D-JAVA
WKD-JAVA
PGPAINLESS
SOP-JAVA
pgpeasy
pgpainless-wot
wot-test-suite
wot-dijkstra
pgpainless-wot-cli
vks-java
vks-java-cli
pgpainless-cert-d
pgpainless-cert-d-cli
pgp-certificate-store
pgp-cert-d-java
pgp-cert-d-java-jdbc-sqlite-lookup
wkd-java
wkd-java-cli
wkd-test-suite
pgpainless-core
pgpainless-sop
pgpainless-cli
sop-java
sop-java-picocli
\ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 00000000..06c115ec --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,38 @@ +PGPainless - Painless OpenPGP +============================= + +**OpenPGP** (`RFC 4480 `_) is an Internet Standard mostly used for email +encryption. +It provides mechanisms to ensure *confidentiality*, *integrity* and *authenticity* of messages. +However, OpenPGP can also be used for other purposes, such as secure messaging or as a signature mechanism for +software distribution. + +**PGPainless** strives to improve the (currently pretty dire) state of the ecosystem of Java libraries and tooling +for OpenPGP. + +The library focuses on being easy and intuitive to use without getting into your way. +Common functions such as creating keys, encrypting data, and so on are implemented using a builder structure that +guides you through the necessary steps. + +Internally, it is based on `Bouncy Castles `_ mighty, but low-level ``bcpg`` +OpenPGP API. +PGPainless' goal is to empower you to use OpenPGP without needing to write all the boilerplate code required by +Bouncy Castle. +It aims to be secure by default while allowing customization if required. + +From its inception in 2018 as part of a `Google Summer of Code project `_, +the library was steadily advanced. +Since 2020, FlowCrypt is the primary sponsor of its development. +In 2022, PGPainless received a `grant from NLnet for creating a Web-of-Trust implementation `_ as part of NGI Assure. + + +Contents +-------- + +.. toctree:: + + ecosystem.md + quickstart.md + pgpainless-cli/usage.md + sop.md + pgpainless-core/indepth.rst \ No newline at end of file diff --git a/docs/source/pgpainless-cli/usage.md b/docs/source/pgpainless-cli/usage.md new file mode 100644 index 00000000..15a87846 --- /dev/null +++ b/docs/source/pgpainless-cli/usage.md @@ -0,0 +1,163 @@ +# User Guide PGPainless-CLI + +The module `pgpainless-cli` contains a command line application which conforms to the +[Stateless OpenPGP Command Line Interface](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/). + +You can use it to generate keys, encrypt, sign and decrypt messages, as well as verify signatures. + +## Implementation + +Essentially, `pgpainless-cli` is just a very small composing module, which injects `pgpainless-sop` as a +concrete implementation of `sop-java` into `sop-java-picocli`. + +## Install + +The `pgpainless-cli` command line application is available in Debian unstable / Ubuntu 22.10 and can be installed via APT: +```shell +$ sudo apt install pgpainless-cli +``` + +This method comes with man-pages: +```shell +$ man pgpainless-cli +``` + +## Build + +To build a standalone *fat*-jar: +```shell +$ cd pgpainless-cli/ +$ gradle shadowJar +``` + +The fat-jar can afterwards be found in `build/libs/`. + +To build a [distributable](https://docs.gradle.org/current/userguide/distribution_plugin.html): + +```shell +$ cd pgpainless-cli/ +$ gradle installDist +``` + +Afterwards, an uncompressed distributable is installed in `build/install/`. +To execute the application, you can call `build/install/bin/pgpainless-cli{.bat}` + +Building / updating man pages is a two-step process. +The contents of the man pages is largely defined by the `sop-java-picocli` source code. + +In order to generate a fresh set of man pages from the `sop-java-picocli` source, you need to clone that repository +next to the `pgpainless` repository: +```shell +$ ls +pgpainless +$ git clone https://github.com/pgpainless/sop-java.git +$ ls +pgpainless sop-java +``` + +Next, you need to execute the `asciiDoctor` gradle task inside the sop-java repository: +```shell +$ cd sop-java +$ gradle asciiDoctor +``` + +This will generate generic sop manpages in `sop-java-picocli/build/docs/manpage/`. + +Next, you need to execute a script for converting the `sop` manpages to fit the `pgpainless-cli` command with the help +of a script in the `pgpainless` repository: +```shell +$ cd ../pgpainless/pgpainless-cli +$ ./rewriteManPages.sh +``` + +The resulting updated man pages are placed in `packaging/man/`. + +## Usage + +Hereafter, the program will be referred to as `pgpainless-cli`. + +``` +$ pgpainless-cli help +Stateless OpenPGP Protocol +Usage: pgpainless-cli [--stacktrace] [COMMAND] + +Options: + --stacktrace Print stacktrace + +Commands: + version Display version information about the tool + list-profiles Emit a list of profiles supported by the identified + subcommand + generate-key Generate a secret key + change-key-password Update the password of a key + revoke-key Generate revocation certificates + extract-cert Extract a public key certificate from a secret key + sign Create a detached message signature + verify Verify a detached signature + encrypt Encrypt a message from standard input + decrypt Decrypt a message + inline-detach Split signatures from a clearsigned message + inline-sign Create an inline-signed message + inline-verify Verify an inline-signed message + armor Add ASCII Armor to standard input + dearmor Remove ASCII Armor from standard input + help Display usage information for the specified subcommand + +Exit Codes: + 0 Successful program execution + 1 Generic program error + 3 Verification requested but no verifiable signature found + 13 Unsupported asymmetric algorithm + 17 Certificate is not encryption capable + 19 Usage error: Missing argument + 23 Incomplete verification instructions + 29 Unable to decrypt + 31 Password is not human-readable + 37 Unsupported Option + 41 Invalid data or data of wrong type encountered + 53 Non-text input received where text was expected + 59 Output file already exists + 61 Input file does not exist + 67 Cannot unlock password protected secret key + 69 Unsupported subcommand + 71 Unsupported special prefix (e.g. "@ENV/@FD") of indirect parameter + 73 Ambiguous input (a filename matching the designator already exists) + 79 Key is not signing capable + 83 Options were supplied that are incompatible with each other + 89 The requested profile is unsupported, or the indicated subcommand does + not accept profiles +``` + +To get help on a subcommand, e.g. `encrypt`, just call the help subcommand followed by the subcommand you +are interested in (e.g. `pgpainless-cli help encrypt`). + +## Examples +```shell +$ # Generate a key +$ pgpainless-cli generate-key "Alice " > key.asc +$ # Extract a certificate from a key +$ cat key.asc | pgpainless-cli extract-cert > cert.asc +$ # Create an encrypted signed message +$ echo "Hello, World!" | pgpainless-cli encrypt cert.asc --sign-with key.asc > msg.asc +$ # Decrypt an encrypted message and verify the signature +$ cat msg.asc | pgpainless-cli decrypt key.asc --verify-with cert.asc --verifications-out verifications.txt +Hello, World! +$ cat verifications.txt +2022-11-15T21:25:48Z 4FF67C69150209ED8139DE22578CB2FABD5D7897 9000235358B8CEA6A368EC86DE56DC2D942ACAA4 +``` + +## Indirect Data Types + +Some commands take options whose arguments are indirect data types. Those are arguments which are not used directly, +but instead they point to a place where the argument value can be sourced from, such as a file, an environment variable +or a file descriptor. + +It is important to keep in mind, that options like `--with-password` or `--with-key-password` are examples for such +indirect data types. If you want to unlock a key whose password is `sw0rdf1sh`, you *cannot* provide the password +like `--with-key-password sw0rdf1sh`, but instead you have to either write out the password into a file and provide +the file's path (e.g. `--with-key-password /path/to/file`), store the password in an environment variable and pass that +(e.g. `--with-key-password @ENV:myvar`), or provide a numbered file descriptor from which the password can be read +(e.g. `--with-key-password @FD:4`). + +Note, that environment variables and file descriptors can only be used to pass input data to the program. +For output parameters (e.g. `--verifications-out`) only file paths are allowed. diff --git a/docs/source/pgpainless-core/edit_keys.md b/docs/source/pgpainless-core/edit_keys.md new file mode 100644 index 00000000..d7ed93e7 --- /dev/null +++ b/docs/source/pgpainless-core/edit_keys.md @@ -0,0 +1 @@ +# Edit Keys diff --git a/docs/source/pgpainless-core/generate_keys.md b/docs/source/pgpainless-core/generate_keys.md new file mode 100644 index 00000000..2b84ce1e --- /dev/null +++ b/docs/source/pgpainless-core/generate_keys.md @@ -0,0 +1,101 @@ +# PGPainless In-Depth: Generate Keys + +There are two API endpoints for generating OpenPGP keys using `pgpainless-core`: + +`PGPainless.generateKeyRing()` presents a selection of pre-configured OpenPGP key archetypes: + +```java +// Modern, EC-based OpenPGP key with dedicated primary certification key +// This method is recommended by the authors +PGPSecretKeyRing secretKey = PGPainless.generateKeyRing() + .modernKeyRing( + "Alice ", + Passphrase.fromPassword("sw0rdf1sh")); + +// Simple, EC-based OpenPGP key with combined certification and signing key +// plus encryption subkey +PGPSecretKeyRing secretKey = PGPainless.generateKeyRing() + .simpleEcKeyRing( + "Alice ", + Passphrase.fromPassword("0r4ng3")); + +// Simple, RSA OpenPGP key made of a single RSA key used for all operations +PGPSecretKeyRing secretKey = PGPainless.generateKeyRing() + .simpleRsaKeyRing( + "Alice ", + RsaLength._4096, Passphrase.fromPassword("m0nk3y")): +``` + +If you have special requirements on algorithms you can use `PGPainless.buildKeyRing()` instead, which offers more +control over parameters: + +```java +// Customized key + +// Specification for primary key +KeySpecBuilder primaryKeySpec = KeySpec.getBuilder( + KeyType.RSA(RsaLength._8192), // 8192 bits RSA key + KeyFlag.CERTIFY_OTHER) // used for certification + // optionally override algorithm preferences + .overridePreferredCompressionAlgorithms(CompressionAlgorithm.ZLIB) + .overridePreferredHashAlgorithms(HashAlgorithm.SHA512, HashAlgorithm.SHA384) + .overridePreferredSymmetricKeyAlgorithms(SymmetricKeyAlgorithm.AES256); + +// Specification for a signing subkey +KeySpecBuilder signingSubKeySpec = KeySpec.getBuilder( + KeyType.ECDSA(EllipticCurve._P256), // P-256 ECDSA key + KeyFlag.SIGN_DATA); // Used for signing + +// Specification for an encryption subkey +KeySpecBuilder encryptionSubKeySpec = KeySpec.getBuilder( + KeyType.ECDH(EllipticCurve._P256), + KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE); + +// Build the key itself +PGPSecretKeyRing secretKey = PGPainless.buildKeyRing() + .setPrimaryKey(primaryKeySpec) + .addSubkey(signingSubKeySpec) + .addSubkey(encryptionSubKeySpec) + .addUserId("Juliet ") // Primary User-ID + .addUserId("xmpp:juliet@capulet.lit") // Additional User-ID + .setPassphrase(Passphrase.fromPassword("romeo_oh_Romeo<3")) // passphrase protection + .build(); +``` + +To specify, which algorithm to use for a single (sub) key, `KeySpec.getBuilder(_)` can be used, passing a `KeyType`, +as well as some `KeyFlag`s as argument. + +`KeyType` defines an algorithm and its parameters, e.g. RSA with a certain key size, or ECDH over a certain +elliptic curve. +Currently, PGPainless supports the following `KeyType`s: +* `KeyType.RSA(_)`: Signing, Certification, Encryption +* `KeyType.ECDH(_)`: Encryption +* `KeyType.ECDSA(_)`: Signing, Certification +* `KeyType.EDDSA(_)`: Signing, Certification +* `KeyType.XDH(_)`: Encryption + +The `KeyFlag`s are used to specify, how the key will be used later on. A signing key can only be used for signing, +if it carries the `KeyFlag.SIGN_DATA`. +A key can carry multiple key flags. + +It is possible to override the default algorithm preferences used by PGPainless with custom preferences. +An algorithm preference list contains algorithms from most to least preferred. + +Every OpenPGP key MUST have a primary key. The primary key MUST be capable of certification, so you MUST use an +algorithm that can be used to generate signatures. +The primary key can be set by calling `setPrimaryKey(primaryKeySpec)`. + +Furthermore, an OpenPGP key can contain zero or more subkeys. +Those can be set by repeatedly calling `addSubkey(subkeySpec)`. + +OpenPGP keys are usually bound to User-IDs like names and/or email addresses. +There can be multiple user-ids bound to a key, in which case the very first User-ID will be marked as primary. +To add a User-ID to the key, call `addUserId(userId)`. + +By default, keys do not have an expiration date. This can be changed by setting an expiration date using +`setExpirationDate(date)`. + +To enable password protection for the OpenPGP key, you can call `setPassphrase(passphrase)`. +If this method is not called, or if the passed in `Passphrase` is empty, the key will be unprotected. + +Finally, calling `build()` will generate a fresh OpenPGP key according to the specifications given. \ No newline at end of file diff --git a/docs/source/pgpainless-core/indepth.rst b/docs/source/pgpainless-core/indepth.rst new file mode 100644 index 00000000..b1c4beba --- /dev/null +++ b/docs/source/pgpainless-core/indepth.rst @@ -0,0 +1,14 @@ +In-Depth Guide to pgpainless-core +================================= + +This is an in-depth introduction to OpenPGP using PGPainless. +If you are looking for a quickstart introduction instead, check out [](quickstart.md). + +Contents +-------- + +.. toctree:: + generate_keys.md + edit_keys.md + userids.md + passphrase.md diff --git a/docs/source/pgpainless-core/passphrase.md b/docs/source/pgpainless-core/passphrase.md new file mode 100644 index 00000000..3127ab25 --- /dev/null +++ b/docs/source/pgpainless-core/passphrase.md @@ -0,0 +1,89 @@ +# Passwords + +In Java based applications, passing passwords as `String` objects has the +[disadvantage](https://stackoverflow.com/a/8881376/11150851) that you have to rely on garbage collection to clean up +once they are no longer used. +For that reason, `char[]` is the preferred method for dealing with passwords. +Once a password is no longer used, the character array can simply be overwritten to remove the sensitive data from +memory. + +## Passphrase +PGPainless uses a wrapper class `Passphrase`, which takes care for the wiping of unused passwords: + +```java +Passphrase passphrase = new Passphrase(new char[] {'h', 'e', 'l', 'l', 'o'}); +assertTrue(passphrase.isValid()); + +assertArrayEquals(new char[] {'h', 'e', 'l', 'l', 'o'}, passphrase.getChars()): + +// Once we are done, we can clean the data +passphrase.clear(); + +assertFalse(passphrase.isValid()); +assertNull(passphrase.getChars()); +``` + +Furthermore, `Passphrase` can also wrap empty passphrases, which increases null-safety of the API: + +```java +Passphrase empty = Passphrase.emptyPassphrase(); +assertTrue(empty.isValid()); +assertTrue(empty.isEmpty()); +assertNull(empty.getChars()); + +empty.clear(); + +assertFalse(empty.isValid()); +``` + +## SecretKeyRingProtector + +There are certain operations that require you to provide the passphrase for a key. +Examples are decryption of messages, or creating signatures / certifications. + +The primary way of telling PGPainless, which password to use for a certain key is the `SecretKeyRingProtector` +interface which maps `Passphrases` to (sub-)keys. +There are multiple implementations of this interface, which may or may not suite your needs: + +```java +// If your key is not password protected, this implementation is for you: +SecretKeyRingProtector unprotected = SecretKeyRingProtector + .unprotectedKeys(); + +// If you use a single passphrase for all (sub-) keys, take this: +SecretKeyRingProtector singlePassphrase = SecretKeyRingProtector + .unlockAnyKeyWith(passphrase); + +// If you want to be flexible, use this: +CachingSecretKeyRingProtector flexible = SecretKeyRingProtector + .defaultSecretKeyRingProtector(passphraseCallback); +``` + +`SecretKeyRingProtector.unprotectedKeys()` will return an empty passphrase for any key. +It is best used when dealing with unencrypted secret keys. + +`SecretKeyRingProtector.unlockAnyKeyWith(passphrase)` will return the same exact passphrase for any given key. +You should use this if you have a single key with a static passphrase. + +The last example shows how to instantiate the `CachingSecretKeyRingProtector` with a `SecretKeyPassphraseProvider` +as argument. +As the name suggests, the `CachingSecretKeyRingProtector` caches passphrases it knows about in a map. +That way, you only have to provide the passphrase for a certain key only once, after which it will be remembered. +If you try to unlock a protected secret key for which no passphrase is cached, the `getPassphraseFor()` method of +the `SecretKeyPassphraseProvider` callback will be called to interactively ask for the missing passphrase. +Afterwards, the acquired passphrase will be cached for future use. + +:::{note} +While especially the `CachingSecretKeyRingProtector` can handle multiple keys without problems, it is advised +to use individual `SecretKeyRingProtector` objects per key. +The reason for this is, that internally the 64bit key-id is used to resolve `Passphrase` objects and collisions are not +unlikely in this key-space. +Furthermore, multiple OpenPGP keys could contain the same subkey, but with different passphrases set. +If the same `SecretKeyRingProtector` is used for two OpenPGP keys with the same subkey, but different passwords, +the key-id collision will cause the password to be overwritten for one of the keys, which might result in issues. +See `FLO-04-004 WP2` of the [2021 security audit](https://cure53.de/pentest-report_pgpainless.pdf) for more details. +::: + +Most `SecretKeyRingProtector` implementations can be instantiated with custom `KeyRingProtectionSettings`. +By default, most implementations use `KeyRingProtectionSettings.secureDefaultSettings()` which corresponds to iterated +and salted S2K using AES256 and SHA256 with an iteration count of 65536. diff --git a/docs/source/pgpainless-core/quickstart.md b/docs/source/pgpainless-core/quickstart.md new file mode 100644 index 00000000..1816983f --- /dev/null +++ b/docs/source/pgpainless-core/quickstart.md @@ -0,0 +1,505 @@ +## PGPainless API with pgpainless-core + +The `pgpainless-core` module contains the bulk of the actual OpenPGP implementation. + +This is a quickstart guide. For more in-depth exploration of the API, checkout [](indepth.md). + +:::{note} +This chapter is work in progress. +::: + +### Setup + +PGPainless' releases are published to and can be fetched from Maven Central. +To get started, you first need to include `pgpainless-core` in your projects build script: + +``` +// If you use Gradle +... +dependencies { + ... + implementation "org.pgpainless:pgpainless-core:XYZ" + ... +} + +// If you use Maven +... + + ... + + org.pgpainless + pgpainless-core + XYZ + + ... + +``` + +This will automatically pull in PGPainless' dependencies, such as Bouncy Castle. + +:::{important} +Replace `XYZ` with the current version, in this case {{ env.config.version }}! +::: + +The entry point to the API is the `PGPainless` class. +For many common use-cases, examples can be found in the +{{ '[examples package](https://{}/main/pgpainless-core/src/test/java/org/pgpainless/example)'.format(repo_pgpainless_src) }}. +There is a very good chance that you can find code examples there that fit your needs. + +### Read and Write Keys +Reading keys from ASCII armored strings or from binary files is easy: + +```java +// Secret Keys +String key = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"...; +PGPSecretKeyRing secretKey = PGPainless.readKeyRing() + .secretKeyRing(key); + +// Certificates (Public Keys) +String cert = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n..."; +PGPPublicKeyRing certificate = PGPainless.readKeyRing() + .publicKeyRing(cert); +``` + +Similarly, keys or certificates can quickly be exported: + +```java +// ASCII armored key +PGPSecretKeyRing secretKey = ...; +String armored = PGPainless.asciiArmor(secretKey); + +// binary (unarmored) key +byte[] binary = secretKey.getEncoded(); +``` + +### Generate a Key +PGPainless comes with a method to quickly generate modern OpenPGP keys. +There are some predefined key archetypes, but it is possible to fully customize the key generation to fit your needs. + +```java +// EdDSA primary key with EdDSA signing- and XDH encryption subkeys +PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() + .modernKeyRing("Romeo ", "thisIsAPassword"); + +// RSA key without additional subkeys +PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() + .simpleRsaKeyRing("Juliet ", RsaLength._4096); +``` + +As you can see, it is possible to generate all kinds of different keys. + +### Extract a Certificate +If you have a secret key, you might want to extract a public key certificate from it: + +```java +PGPSecretKeyRing secretKey = ...; +PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKey); +``` + +### Apply / Remove ASCII Armor +ASCII armor is a layer of radix64 encoding that can be used to wrap binary OpenPGP data in order to make it save to +transport via text-based channels (e.g. email bodies). + +The way in which ASCII armor can be applied depends on the type of data that you want to protect. +The easies way to ASCII armor an OpenPGP key or certificate is by using PGPainless' `asciiArmor()` method: + +```java +PGPPublicKey certificate = ...; +String asciiArmored = PGPainless.asciiArmor(certificate); +``` + +If you want to ASCII armor ciphertext, you can enable ASCII armoring during encrypting/signing by requesting +PGPainless to armor the result: + +```java +ProducerOptions producerOptions = ...; // prepare as usual (see next section) + +producerOptions.setAsciiArmor(true); // enable armoring + +EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() + .onOutputStream(out) + .withOptions(producerOptions); + +... +``` + +If you have an already encrypted / signed binary message and want to add ASCII armoring retrospectively, you need +to make use of BouncyCastle's `ArmoredOutputStream` as follows: + +```java +InputStream binaryOpenPgpIn = ...; // e.g. new ByteArrayInputStream(binaryMessage); + +OutputStream output = ...; // e.g. new ByteArrayOutputStream(); +ArmoredOutputStream armorOut = ArmoredOutputStreamFactory.get(output); + +Streams.pipeAll(binaryOpenPgpIn, armorOut); +armorOut.close(); // important! +``` + +The output stream will now contain the ASCII armored representation of the binary data. + +If the data you want to wrap in ASCII armor is non-OpenPGP data (e.g. the String "Hello World!"), +you need to use the following code: + +```java +InputStream inputStream = ...; +OutputStream output = ...; + +EncryptionStream armorStream = PGPainless.encryptAndOrSign() + .onOutputStream(output) + .withOptions(ProducerOptions.noEncryptionNoSigning() + .setAsciiArmor(true)); + +Streams.pipeAll(inputStream, armorStream); +armorStream.close(); +``` + +To remove ASCII armor, you can make use of BouncyCastle's `ArmoredInputStream` as follows: + +```java +InputStream input = ...; // e.g. new ByteArrayInputStream(armoredString.getBytes(StandardCharsets.UTF8)); +OutputStream output = ...; + +ArmoredInputStream armorIn = new ArmoredInputStream(input); +Streams.pipeAll(armorIn, output); +armorIn.close(); +``` + +The output stream will now contain the binary OpenPGP data. + +### Encrypt and/or Sign a Message +Encrypting and signing messages is done using the same API in PGPainless. +The type of action depends on the configuration of the `ProducerOptions` class, which in term accepts +`SigningOptions` and `EncryptionOptions` objects: + +```java +// Encrypt only +ProducerOptions options = ProducerOptions.encrypt(encryptionOptions); + +// Sign only +ProducerOptions options = ProducerOptions.sign(signingOptions); + +// Sign and encrypt +ProducerOptions options = ProducerOptions.signAndEncrypt(signingOptions, encryptionOptions); +``` + +The `ProducerOptions` object can then be passed into the `encryptAndOrSign()` API: + +```java +InputStream plaintext = ...; // The data that shall be encrypted and/or signed +OutputStream ciphertext = ...; // Destination for the ciphertext + +EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() + .onOutputStream(ciphertext) + .withOptions(options); // pass in the options object + +Streams.pipeAll(plaintext, encryptionStream); // pipe the data through +encryptionStream.close(); // important! Close the stream to finish encryption/signing + +EncryptionResult result = encryptionStream.getResult(); // metadata +``` + +The `ciphertext` output stream now contains the encrypted and/or signed data. + +Now lets take a look at the configuration of the `SigningOptions` object and how to instruct PGPainless to add a simple +signature to the message: + +```java +PGPSecretKeyRing signingKey = ...; // Key used for signing +SecretKeyRingProtector protector = ...; // Protector to unlock the signing key + +SigningOptions signOptions = SigningOptions.get() + .addSignature(protector, signingKey); +``` +This will add an inline signature to the message. + +It is possible to add multiple signatures from different keys by repeating the `addSignature()` method call. + +If instead of an inline signature, you want to create a detached signature instead (e.g. because you do not want +to alter the data you are signing), you can add the signature as follows: + +```java +signOptions.addDetachedSignature(protector, signingKey); +``` + +Passing in the `SigningOptions` object like this will result in the signature not being added to the message itself. +Instead, the signature can later be acquired from the `EncryptionResult` object via `EncryptionResult.getDetachedSignatures()`. +That way, it can be distributed independent of the message. + +The `EncryptionOptions` object can be configured in a similar way: + +```java +PGPPublicKey certificate = ...; + +EncryptionOptions encOptions = EncryptionOptions.get() + .addRecipient(certificate); +``` + +Once again, it is possible to add multiple recipients by repeating the `addRecipient()` method call. + +In order to prevent metadata leaks, you might want to add recipients anonymously. +Anonymous recipients have their key-id hidden by replacing it with a wildcard. +That way, it is not easily possible for an attacker to deduce the recipients of a message without further +analysis of additional metadata. +Anonymous recipients can be added like follows: +```java +encOptions.addHiddenRecipient(certificate); +``` + +You can also encrypt a message to a password like this: +```java +encOptions.addPassphrase(Passphrase.fromPassword("sw0rdf1sh")); +``` + +Both methods can be used in combination to create a message which can be decrypted with either a recipients secret key +or the passphrase. + +### Decrypt and/or Verify a Message +Decryption and verification of a message is both done using the same API. +Whether a message was actually signed / encrypted can be determined after the message has been processed by checking +the `MessageMetadata` object which can be obtained from the `DecryptionStream`. + +To configure the decryption / verification process, the `ConsumerOptions` object is used: + +```java +PGPPublicKeyRing verificationCert = ...; // optional, signers certificate for signature verification +PGPSecretKeyRing decryptionKey = ...; // optional, decryption key + +ConsumerOptions options = ConsumerOptions.get() + .addVerificationCert(verificationCert) // add a verification cert for signature verification + .addDecryptionKey(decryptionKey); // add a secret key for message decryption +``` + +Both verification certificates and decryption keys are optional. +If you know the message is signed, but not encrypted you can omit providing a decryption key. +Same goes for if you know that the message is encrypted, but not signed. +In this case you can omit the verification certificate. + +On the other hand, providing these parameters does not hurt. +PGPainless will ignore unused keys / certificates, so if you provide a decryption key and the message is not encrypted, +nothing bad will happen. + +It is possible to provide multiple verification certs and decryption keys. PGPainless will pick suitable ones on the fly. +If the message is signed with key `0xAAAA` and you provide certificates `0xAAAA` and `0xBBBB`, it will verify +with cert `0xAAAA` and ignore `0xBBBB`. + +To do the actual decryption / verification of the message, do the following: + +```java +InputStream ciphertext = ...; // encrypted and/or signed message +OutputStream plaintext = ...; // destination for the plaintext + +ConsumerOptions options = ...; // see above +DecryptionStream consumerStream = PGPainless.decryptAndOrVerify() + .onInputStream(ciphertext) + .withOptions(options); + +Streams.pipeAll(consumerStream, plaintext); +consumerStream.close(); // important! + +// The result will contain metadata of the message +MessageMetadata result = consumerStream.getMetadata(); +``` + +After the message has been processed, you can consult the `MessageMetadata` object to determine the nature of the message: + +```java +boolean wasEncrypted = result.isEncrypted(); +SubkeyIdentifier decryptionKey = result.getDecryptionKey(); +List validSignatures = result.getVerifiedSignatures(); +boolean wasSignedByCert = result.isVerifiedSignedBy(certificate); + +// For files: +String fileName = result.getFileName(); +Date modificationData = result.getModificationDate(); +``` + +### Verify a Signature +In some cases, detached signatures are distributed alongside the message. +This is the case for example with Debians `Release` and `Release.gpg` files. +Here, `Release` is the plaintext message, which is unaltered by the signing process while `Release.gpg` contains +the detached OpenPGP signature. + +To verify a detached signature, you need to call the PGPainless API like this: + +```java +InputStream plaintext = ...; // e.g. new FileInputStream(releaseFile); +InputStream detachedSignature = ...; // e.g. new FileInputStream(releaseGpgFile); +PGPPublicKeyRing certificate = ...; // e.g. debians public signing key + +ConsumerOptions options = ConsumerOptions.get() + .addVerificationCert(certificate) // provide certificate for verification + .addVerificationOfDetachedSignatures(detachedSignature) // provide detached signature + +DecryptionStream verificationStream = PGPainless.decryptAndOrVerify() + .onInputStream(plaintext) + .withOptions(options); + +Streams.drain(verificationStream); // push all the data through the stream +verificationStream.close(); // finish verification + +MessageMetadata result = verificationStream.getMetadata(); // get metadata of signed message +assertTrue(result.isVerifiedSignedBy(certificate)); // check if message was in fact signed +``` + +### Legacy Compatibility +Out of the box, PGPainless is configured to use secure defaults and perform checks for recommended +security features. This means that for example messages generated using older OpenPGP +implementations which do not follow those best practices might fail to decrypt/verify. + +It is however possible to circumvent certain security checks to allow processing of such messages. + +:::{note} +It is not recommended to disable security checks, as that might enable certain attacks on the OpenPGP protocol. +::: + +#### Missing / broken MDC (modification detection code) +RFC4880 has two different types of encrypted data packets. The *Symmetrically Encrypted Data* packet (SED) and the *Symmetrically Encrypted Integrity-Protected Data* packet. +The latter has an added MDC packet which prevents modifications to the ciphertext. + +While implementations are highly encouraged to only use the latter package type, some older implementations still generate +encrypted data packets which are not integrity protected. + +To allow PGPainless to decrypt such messages, you need to set a flag in the `ConsumerOptions` object: +```java +ConsumerOptions options = ConsumerOptions.get() + .setIgnoreMDCErrors(true) // <- + .setDecryptionKey(secretKey) + ... + +DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(ciphertextIn) + .withOptions(options); +... +``` + +:::{note} +It is highly advised to only set this flag if you know what you are doing. +It might also be a good idea to try decrypting a message without the flag set first and only re-try +decryption with the flag set in case of a `MessageNotIntegrityProtectedException` (don't forget to rewind the ciphertextInputStream). +::: + +#### Weak keys and broken algorithms +Some users might cling on to older keys using weak algorithms / small key sizes. +PGPainless refuses to encrypt to weak certificates and sign with weak keys. +By default, PGPainless follows the recommendations for acceptable key sizes of [the German BSI in 2021](https://www.bsi.bund.de/SharedDocs/Downloads/EN/BSI/Publications/TechGuidelines/TG02102/BSI-TR-02102-1.pdf). +It can however be configured to accept older key material / algorithms too. + +Minimal key lengths can be configured by changing PGPainless' policy: +```java +Map algorithms = new HashMap<>(); +// put all acceptable algorithms and their minimal key length +algorithms.put(PublicKeyAlgorithm.RSA_GENERAL, 1024); +algorithms.put(PublicKeyAlgorithm.ECDSA, 100); +... +Policy.PublicKeyAlgorithmPolicy pkPolicy = + new Policy.PublicKeyAlgorithmPolicy(algorithms); +// set the custom algorithm policy +PGPainless.getPolicy().setPublicKeyAlgorithmPolicy(); +``` + +Since OpenPGP uses a hybrid encryption scheme of asymmetric and symmetric encryption algorithms, +it also comes with a policy for symmetric encryption algorithms. +This list can be modified to allow for weaker algorithms like follows: +```java +// default fallback algorithm for message encryption +SymmetricKeyAlgorithm fallbackAlgorithm = SymmetricKeyAlgorithm.AES_256; +// acceptable algorithms +List algorithms = new ArrayList<>(); +algorithms.add(SymmetricKeyAlgorithm.AES_256); +algorithms.add(SymmetricKeyAlgorithm.AES_192); +algorithms.add(SymmetricKeyAlgorithm.AES_128); +algorithms.add(SymmetricKeyAlgorithm.TWOFISH); +algorithms.add(SymmetricKeyAlgorithm.BLOWFISH); +... +Policy.SymmetricKeyAlgorithmPolicy skPolicy = + new SymmtricKeyAlgorithmPolicy(fallbackAlgorithm, algorithms); +// set the custom algorithm policy +// algorithm policy applicable when decrypting messages created by legacy senders: +PGPainless.getPolicy() + .setSymmetricKeyDecryptionAlgorithmPolicy(skPolicy); +// algorithm policy applicable when generating messages for legacy recipients: +PGPainless.getPolicy() + .setSymmetricKeyEncryptionAlgorithmPolicy(skPolicy); +``` + +Hash algorithms are used in OpenPGP to create signatures. +Since signature verification is an integral part of the OpenPGP protocol, PGPainless comes +with multiple policies for acceptable hash algorithms, depending on the use-case. +Revocation signatures are critical, so you might want to handle revocation signatures differently from normal signatures. + +By default, PGPainless uses a smart hash algorithm policy for both use-cases, which takes into consideration +not only the hash algorithm itself, but also the creation date of the signature. +That way, signatures using SHA-1 are acceptable if they were created before February 2013, but are rejected if their +creation date is after that point in time. + +A custom hash algorithm policy can be set like this: +```java +HashAlgorithm fallbackAlgorithm = HashAlgorithm.SHA512; +Map algorithms = new HashMap<>(); +// Accept MD5 on signatures made before 1997-02-01 +algorithms.put(HashAlgorithm.MD5, + DateUtil.parseUTCDate("1997-02-01 00:00:00 UTC")); +// Accept SHA-1, regardless of signature creation time +algorithms.put(HashAlgorithm.SHA1, null); +... +Policy.HashAlgorithmPolicy hPolicy = + new Policy.HashAlgorithmPolicy(fallbackAlgorithm, algorithms); +// set policy for revocation signatures +PGPainless.getPolicy() + .setRevocationSignatureHashAlgorithmPolicy(hPolicy); +// set policy for normal signatures (certifications and document signatures) +PGPainless.getPolicy() + .setSignatureHashAlgorithmPolicy(hPolicy); +``` + +Lastly, PGPainless comes with a policy on acceptable compression algorithms, which currently accepts any +compression algorithm. +A custom compression algorithm policy can be set in a similar way: +```java +CompressionAlgorithm fallback = CompressionAlgorithm.ZIP; +List algorithms = new ArrayList<>(); +algorithms.add(CompressionAlgorith.ZIP); +algorithms.add(CompressionAlgorithm.BZIP2); +... +Policy.CompressionAlgorithmPolicy cPolicy = + new Policy.CompressionAlgorithmPolicy(fallback, algorithms); +PGPainless.getPolicy() + .setCompressionAlgorithmPolicy(cPolicy); +``` + +To prevent a class of attacks described in the [paper](https://www.kopenpgp.com/#paper) +"Victory by KO: Attacking OpenPGP Using Key Overwriting", +PGPainless offers the option to validate private key material each time before using it, +to make sure that an attacker didn't tamper with the corresponding public key parameters. + +These checks are disabled by default, but they can be enabled as follows: +```java +PGPainless.getPolicy() + .setEnableKeyParameterValidation(true); +``` + +:::{note} +Validation checks against KOpenPGP attacks are disabled by default, since they are very costly +and only make sense in certain scenarios. +Please read and understand the paper to decide, if enabling the checks makes sense for your use-case. +::: + + +### Known Notations +In OpenPGP, signatures can contain [notation subpackets](https://www.rfc-editor.org/rfc/rfc4880#section-5.2.3.16). +A notation can give meaning to a signature, or add additional contextual information. +Signature subpackets can be marked as critical, meaning an implementation that does not know about +a certain subpacket MUST reject the signature. +The same is true for critical notations. + +For that reason, PGPainless comes with a `NotationRegistry` class which can be used to register known notations, +such that a signature containing a critical notation of a certain value is not rejected. +To register a known notation, you can do the following: + +```java +NotationRegistry registry = PGPainless.getPolicy() + .getNotationRegistry(); + +registry.addKnownNotation("sample@example.com"); +``` \ No newline at end of file diff --git a/docs/source/pgpainless-core/userids.md b/docs/source/pgpainless-core/userids.md new file mode 100644 index 00000000..d2b5730e --- /dev/null +++ b/docs/source/pgpainless-core/userids.md @@ -0,0 +1,47 @@ +# User-IDs + +User-IDs are identities that users go by. A User-ID might be a name, an email address or both. +User-IDs can also contain both and even have a comment. + +In general, the format of a User-ID is not fixed, so it can contain arbitrary strings. +However, it is agreed upon to use the +Below is a selection of possible User-IDs: + +``` +Firstname Lastname (Comment) +Firstname Lastname +Firstname Lastname (Comment) + +``` + +PGPainless comes with a builder class `UserId`, which can be used to safely construct User-IDs: + +```java +UserId nameAndEMail = UserId.nameAndEmail("Jane Doe", "jane@pgpainless.org"); +assertEquals("Jane Doe ", nameAndEmail.toString()): + +UserId onlyEmail = UserId.onlyEmail("john@pgpainless.org"); +assertEquals("", onlyEmail.toString()); + +UserId full = UserId.newBuilder() + .withName("Peter Pattern") + .withEmail("peter@pgpainless.org") + .withComment("Work Address") + .build(); +assertEquals("Peter Pattern (Work Address) ", full.toString()); +``` + +If you have a User-ID in form of a string (e.g. because a user provided it via a text field), +you can parse it into its components like this: + +```java +String string = "John Doe "; +UserId userId = UserId.parse(string); + +// Now you can access the different components +assertEquals("John Doe", userId.getName()); +assertEquals("john@doe.corp", userId.getEmail()); +assertNull(userId.getComment()); +``` + +The method `UserId.parse(String string)` will throw an `IllegalArgumentException` if the User-ID is malformed. diff --git a/docs/source/pgpainless-sop/quickstart.md b/docs/source/pgpainless-sop/quickstart.md new file mode 100644 index 00000000..55958ee4 --- /dev/null +++ b/docs/source/pgpainless-sop/quickstart.md @@ -0,0 +1,546 @@ +## SOP API with pgpainless-sop + +The Stateless OpenPGP Protocol (SOP) defines a simplistic interface for the most important OpenPGP operations. +It allows you to encrypt, decrypt, sign and verify messages, generate keys and add/remove ASCII armor from data. +However, it does not yet provide tools for key management. +Furthermore, the implementation is deciding for you, which (secure) algorithms to use, and it doesn't let you +change those. + +If you want to read more about the background of the SOP protocol, there is a [whole chapter](../sop) dedicated to it. + +### Setup + +PGPainless' releases are published to and can be fetched from Maven Central. +To get started, you first need to include `pgpainless-sop` in your projects build script. +``` +// If you use Gradle +... +dependencies { + ... + implementation "org.pgpainless:pgpainless-sop:XYZ" + ... +} + +// If you use Maven +... + + ... + + org.pgpainless + pgpainless-sop + XYZ + + ... + +``` + +:::{important} +Replace `XYZ` with the current version, in this case {{ env.config.version }}! +::: + +The entry point to the API is the `SOP` interface, for which `pgpainless-sop` provides a concrete implementation +`SOPImpl`. + +```java +// Instantiate the API +SOP sop = new SOPImpl(); +``` + +Now you are ready to go! + +### Generate a Key + +To generate a new OpenPGP key, the method `SOP.generateKey()` is your friend: + +```java +// generate key +byte[] keyBytes = sop.generateKey() + .userId("John Doe ") + .withKeyPassword("f00b4r") + .generate() + .getBytes(); +``` + +The call `userId(String userId)` can be called multiple times to add multiple user-ids to the key, but it MUST +be called at least once. +The argument given in the first invocation will become the keys primary user-id. + +Optionally, the key can be protected with a password by calling `withKeyPassword(String password)`. +If this method is not called, the key will be unprotected. + +The `generate()` method call generates the key and returns a `Ready` object. +This in turn can be used to write the result to a stream via `writeTo(OutputStream out)`, or to get the result +as bytes via `getBytes()`. +In both cases, the resulting output will be the UTF8 encoded, ASCII armored OpenPGP secret key. + +To disable ASCII armoring, call `noArmor()` before calling `generate()`. + +Revision `05` of the Stateless OpenPGP Protocol specification introduced the concept of profiles for +certain operations. +The key generation feature is the first operation to make use of profiles to specify different key algorithms. +To set a profile, simply call `profile(String profileName)` and pass in one of the available profile identifiers. + +To explore, which profiles are available, refer to the dedicated [section](#explore-profiles). + +The default profile used by `pgpainless-sop` is called `draft-koch-eddsa-for-openpgp-00`. +If this profile is used, the resulting OpenPGP secret key will consist of a certification-capable 256-bits +ed25519 EdDSA primary key, a 256-bits ed25519 EdDSA subkey used for signing, as well as a 256-bits X25519 +ECDH subkey for encryption. + +Another profile defined by `pgpainless-sop` is `rfc4880`, which changes the key generation behaviour such that +the resulting key is a single 4096-bit RSA key capable of certifying, signing and encrypting. + +The whole key does not have an expiration date set. + +### Extract a Certificate + +Now that you generated your secret key, you probably want to share the public key with your contacts. +To extract the OpenPGP public key (which we will call *certificate* from now on) from the secret key, +use the `SOP.extractCert()` method call: + +```java +// extract certificate +byte[] certificateBytes = sop.extractCert() + .key(keyBytes) + .getBytes(); +``` + +The `key(_)` method either takes a byte array (like in the example), or an `InputStream`. +In both cases it returns another `Ready` object from which the certificate can be accessed, either via +`writeTo(OutputStream out)` or `getBytes()`. + +By default, the resulting certificate will be ASCII armored, regardless of whether the input key was armored or not. +To disable ASCII armoring, call `noArmor()` before calling `key(_)`. + +In our example, `certificateBytes` can now safely be shared with anyone. + +### Change Key Password + +OpenPGP keys can (but don't need to) be password protected. +The `changeKeyPassword()` API can be used to add, change or remove password protection from OpenPGP keys. +While the input to this operation can be keys with different per-subkey passwords, the output will use at most one password. + +Via `oldKeyPassphrase()`, multiple decryption passphrase candidates can be provided. +These are tried one after another to unlock protected subkeys. + +In order to successfully change the passphrase of an OpenPGP key, all of its subkeys needs to be successfully decrypted. +If one or more subkeys cannot be decrypted, the operation fails with a `KeyIsProtected` exception. +The result is either fully encrypted for a single passphrase (passed via `newKeyPassphrase()`), +or unprotected if the new key passphrase is omitted. + + +```java +byte[] keyBefore = ... +byte[] keyAfter = sop.changeKeyPassword() + // Provide old passphrases - all subkeys need to be decryptable, + // otherwise KeyIsProtected exception will be thrown + .oldKeyPassphrase("4d4m5m1th") + .oldKeyPassphrase("d4v1dR1c4rd0") + // Provide the new passphrase - if omitted, key will be unprotected + .newKeyPassphrase("fr1edr1ch3n93l5") + .keys(keyBefore) + .getBytes(); +``` + +### Generate Revocation Certificates + +You might want to generate a revocation certificate for your OpenPGP key. +This certificate can be published to a key server to let your contacts known that your key is no longer +trustworthy. +The `revokeKey()` API can be used to generate a "hard-revocation", which retroactively invalidates all +signatures previously issued by the key. + +If the input secret key is an OpenPGP v6 key, the result will be a minimal revocation certificate, +consisting of only the bare primary public key and a revocation signature. For v4 keys, the result +will consist of the whole public certificate plus a revocation signature. + +```java +byte[] keys = ... +byte[] revoked = sop.revokeKey() + // primary key password(s) if the key(s) are protected + .withKeyPassword("5w0rdf1sh") + // one or more secret keys + .keys(keys) + .getBytes(); +``` + +### Apply / Remove ASCII Armor + +Perhaps you want to print your secret key onto a piece of paper for backup purposes, +but you accidentally called `noArmor()` when generating the key. + +To add ASCII armor to some binary OpenPGP data, the `armor()` API can be used: + +```java +// wrap data in ASCII armor +byte[] armoredData = sop.armor() + .data(binaryData) + .getBytes(); +``` + +The `data(_)` method can either be called by providing a byte array, or an `InputStream`. + +To remove ASCII armor from armored data, simply use the `dearmor()` API: + +```java +// remove ASCII armor +byte[] binaryData = sop.unarmor() + .data(armoredData) + .getBytes(); +``` + +Once again, the `data(_)` method can be called either with a byte array or an `InputStream` as argument. + +If the input data is not validly armored OpenPGP data, the `data(_)` method call will throw a `BadData` exception. + +### Encrypt a Message + +Now lets get to the juicy part and finally encrypt a message! +In this example, we will assume that Alice is the sender that wants to send a message to Bob. +Beforehand, Alice acquired Bobs certificate, e.g. by fetching it from a key server. + +To encrypt a message, you can make use of the `encrypt()` API: + +```java +// encrypt and sign a message +byte[] aliceKey = ...; // Alice' secret key +byte[] aliceCert = ...; // Alice' certificate (e.g. via extractCert()) +byte[] bobCert = ...; // Bobs certificate + +byte[] plaintext = "Hello, World!\n".getBytes(); // plaintext + +byte[] ciphertext = sop.encrypt() + // encrypt for each recipient + .withCert(bobCert) + .withCert(aliceCert) + // Optionally: Sign the message + .signWith(aliceKey) + .withKeyPassword("sw0rdf1sh") // if signing key is protected + // provide the plaintext + .plaintext(plaintext) + .getBytes(); +``` + +Here you encrypt the message for each recipient (Alice probably wants to be able to decrypt the message too!) +by calling `withCert(_)` with the recipients certificate as argument. It does not matter, if the certificate +is ASCII armored or not, and the method can either be called with a byte array or an `InputStream` as argument. + +The API not only supports asymmetric encryption via OpenPGP certificates, but it can also encrypt messages +symmetrically using one or more passwords. Both mechanisms can even be used together in the same message! +To (additionally or exclusively) encrypt the message for a password, simply call `withPassword(String password)` +before the `plaintext(_)` method call. + +It is recommended (but not required) to sign encrypted messages. +In order to sign the message before encryption is applied, call `signWith(_)` with the signing key as argument. +This method call can be repeated multiple times to sign the message with multiple signing keys. + +If any keys used for signing are password protected, you need to provide the signing key password via +`withKeyPassword(_)`. +It does not matter in which order signing keys and key passwords are provided, the implementation will figure out +matches on its own. If different key passwords are used, the `withKeyPassword(_)` method can be called multiple times. + +You can modify the behaviour of the encrypt operation by switching between different profiles via the +`profile(String profileName)` method. +At the time of writing, the only available profile for this operation is `rfc4880` which applies encryption +as defined in [rfc4880](https://datatracker.ietf.org/doc/html/rfc4880). + +To explore, which profiles are available, refer to the dedicated [section](#explore-profiles). + +By default, the encrypted message will be ASCII armored. To disable ASCII armor, call `noArmor()` before the +`plaintext(_)` method call. + +Lastly, you need to provide the plaintext by calling `plaintext(_)` with either a byte array or an `InputStream` +as argument. +The ciphertext can then be accessed from the resulting `Ready` object as usual. + +### Decrypt a Message + +Now let's switch perspective and help Bob decrypt the message from Alice. + +Decrypting encrypted messages is done in a similar fashion using the `decrypt()` API: + +```java +// decrypt a message and verify its signature(s) +byte[] aliceCert = ...; // Alice' certificate +byte[] bobKey = ...; // Bobs secret key +byte[] bobCert = ...; // Bobs certificate + +byte[] ciphertext = ...; // the encrypted message + +ReadyWithResult readyWithResult = sop.decrypt() + .withKey(bobKey) + .verifyWithCert(aliceCert) + .withKeyPassword("password123") // if decryption key is protected + .ciphertext(ciphertext); +``` + +The `ReadyWithResult` can now be processed in two different ways, depending on whether you want the +plaintext as bytes or simply write it out to an `OutputStream`. + +To get the plaintext bytes directly, you shall proceed as follows: + +```java +ByteArrayAndResult bytesAndResult = readyWithResult.toByteArrayAndResult(); +DecryptionResult result = bytesAndResult.getResult(); +byte[] plaintext = bytesAndResult.getBytes(); +``` + +If you instead want to write the plaintext out to an `OutputStream`, the following code can be used: + +```java +OutputStream out = ...; +DecryptionResult result = readyWithResult.writeTo(out); +``` + +Note, that in both cases you acquire a `DecryptionResult` object. This contains information about the message, +such as which signatures could successfully be verified. + +If you provided the senders certificate for the purpose of signature verification via `verifyWith(_)`, you now +probably want to check, if the message was actually signed by the sender by checking `result.getVerifications()`. + +:::{note} +Signature verification will be discussed in more detail in section "Verifications". +::: + +If the message was encrypted symmetrically using a password, you can also decrypt is symmetrically by calling +`withPassword(String password)` before the `ciphertext(_)` method call. This method call can be repeated multiple +times. The implementation will try different passwords until it finds a matching one. + +### Sign a Message + +There are three different main ways of signing a message: +* Inline Signatures +* Cleartext Signatures +* Detached Signatures + +An inline-signature will be part of the message itself (e.g. like with messages that are encrypted *and* signed). +Inline-signed messages are not human-readable without prior processing. + +A cleartext signature makes use of the [cleartext signature framework](https://datatracker.ietf.org/doc/html/rfc4880#section-7). +Messages signed in this way do have an ASCII armor header and footer, yet the content of the message is still +human-readable without special software. + +Lastly, a detached signature can be distributed as an extra file alongside the message without altering it. +This is useful if the plaintext itself cannot be modified (e.g. if a binary file is signed). + +The SOP API can generate all of those signature types. + +#### Inline-Signatures + +Let's start with an inline signature: + +```java +byte[] signingKey = ...; +byte[] message = ...; + +byte[] inlineSignedMessage = sop.inlineSign() + .mode(InlineSignAs.Text) // or 'Binary' + .key(signingKey) + .withKeyPassword("fnord") + .data(message) + .getBytes(); +``` + +You can choose between two different signature formats which can be set using `mode(InlineSignAs mode)`. +The default value is `Binary`. You can also set it to `Text` which signals to the receiver that the data is +UTF8 text. + +:::{note} +For inline signatures, do NOT set the `mode()` to `CleartextSigned`, as that will create message which uses the +cleartext signature framework (see further below). +::: + +You must provide at least one signing key using `key(_)` in order to be able to sign the message. + +If any key is password protected, you need to provide its password using `withKeyPassword(_)` which +can be called multiple times to provide multiple passwords. + +Once you provide the plaintext using `data(_)` with either a byte array or an `InputStream` as argument, +you will get a `Ready` object back, from which the signed message can be retrieved as usual. + +By default, the signed message will be ASCII armored. This can be disabled by calling `noArmor()` +before the `data(_)` method call. + +#### Cleartext Signatures + +A cleartext-signed message can be generated in a similar way to an inline-signed message, however, +there are is one subtle difference: + +```java +byte[] signingKey = ...; +byte[] message = ...; + +byte[] cleartextSignedMessage = sop.inlineSign() + .mode(InlineSignAs.CleartextSigned) // This MUST be set + .key(signingKey) + .withKeyPassword("fnord") + .data(message) + .getBytes(); +``` + +:::{important} +In order to produce a cleartext-signed message, the signature mode MUST be set to `CleartextSigned` +by calling `mode(InlineSignAs.CleartextSigned)`. +::: + +:::{note} +Calling `noArmor()` will have no effect for cleartext-signed messages, so such method call will be ignored. +::: + +#### Detached Signatures + +As the name suggests, detached signatures are detached from the message itself and can be distributed separately. + +To produce a detached signature, the `detachedSign()` API is used: + +```java +byte[] signingKey = ...; +byte[] message = ...; + +ReadyWithResult readyWithResult = sop.detachedSign() + .key(signingKey) + .withKeyPassword("fnord") + .data(message); +``` + +Here you have the choice, how you want to write out the signature. +If you want to write the signature to an `OutputStream`, you can do the following: + +```java +OutputStream out = ...; +SigningResult result = readyWithResult.writeTo(out); +``` + +If instead you want to get the signature as a byte array, do this instead: + +```java +ByteArrayAndResult bytesAndResult = readyWithResult.toByteArrayAndResult(); +SigningResult result = bytesAndResult.getResult(); +byte[] detachedSignature = bytesAndResult.getBytes(); +``` + +In any case, the detached signature can now be distributed alongside the original message. + +By default, the resulting detached signature will be ASCII armored. This can be disabled by calling `noArmor()` +prior to calling `data(_)`. + +The `SigningResult` object you got back in both cases contains information about the signature. + +### Verify a Signature + +In order to verify signed messages, there are two API endpoints available. + +#### Inline and Cleartext Signatures + +To verify inline-signed messages, or messages that make use of the cleartext signature framework, +use the `inlineVerify()` API: + +```java +byte[] signingCert = ...; +byte[] signedMessage = ...; + +ReadyWithResult> readyWithResult = sop.inlineVerify() + .cert(signingCert) + .data(signedMessage); +``` + +The `cert(_)` method MUST be called at least once. It takes either a byte array or an `InputStream` containing +an OpenPGP certificate. +If you are not sure, which certificate was used to sign the message, you can provide multiple certificates. + +It is also possible to reject signatures that were not made within a certain time window by calling +`notBefore(Date timestamp)` and/or `notAfter(Date timestamp)`. +Signatures made before the `notBefore(_)` or after the `notAfter(_)` constraints will be rejected. + +You can now either write out the plaintext message to an `OutputStream`... + +```java +OutputStream out = ...; +List verifications = readyWithResult.writeTo(out); +``` + +... or you can acquire the plaintext message as a byte array directly: + +```java +ByteArrayAndResult> bytesAndResult = readyWithResult.toByteArrayAndResult(); +byte[] plaintextMessage = bytesAndResult.getBytes(); +List verifications = bytesAndResult.getResult(); +``` + +In both cases, the plaintext message will have the signatures stripped. + +#### Detached Signatures + +To verify detached signatures (signatures that come separate from the message itself), you can use the +`detachedVerify()` API: + +```java +byte[] signingCert = ...; +byte[] message = ...; +byte[] detachedSignature = ...; + +List verifications = sop.detachedVerify() + .cert(signingCert) + .signatures(detachedSignature) + .data(signedMessage); +``` + +You can provide one or more OpenPGP certificates using `cert(_)`, providing either a byte array or an `InputStream`. + +The detached signatures need to be provided separately using the `signatures(_)` method call. +You can provide as many detached signatures as you like, and those can be binary or ASCII armored. + +Like with Inline Signatures, you can constrain the time window for signature validity using +`notAfter(_)` and `notBefore(_)`. + +#### Verifications + +In all above cases, the `verifications` list will contain `Verification` objects for each verifiable, valid signature. +Those objects contain information about the signatures: +`verification.getSigningCertFingerprint()` will return the fingerprint of the certificate that created the signature. +`verification.getSigningKeyFingerprint()` will return the fingerprint of the used signing subkey within that certificate. + +### Detach Signatures from Messages + +It is also possible, to detach inline or cleartext signatures from signed messages to transform them into +detached signatures. +The same way you can turn inline or cleartext signed messages into plaintext messages. + +To detach signatures from messages, use the `inlineDetach()` API: + +```java +byte[] signedMessage = ...; + +ReadyWithResult readyWithResult = sop.inlineDetach() + .message(signedMessage); +ByteArrayAndResult bytesAndResult = readyWithResult.toByteArrayAndResult(); + +byte[] plaintext = bytesAndResult.getBytes(); +Signatures signatures = bytesAndResult.getResult(); +byte[] encodedSignatures = signatures.getBytes(); +``` + +By default, the signatures output will be ASCII armored. This can be disabled by calling `noArmor()` +prior to `message(_)`. + +The detached signatures can now be verified like in the section above. + +### Explore Profiles + +Certain operations allow modification of their behaviour by selecting between different profiles. +An example for this is the `generateKey()` operation, where different profiles result in different algorithms used +during key generation. + +To explore, which profiles are supported by a certain operation, you can use the `listProfiles()` operation. +For example, this is how you can get a list of profiles supported by the `generateKey()` operation: + +```java +List profiles = sop.listProfiles().subcommand("generate-key"); +``` + +:::{note} +As you can see, the argument passed into the `subcommand()` method must match the operation name as defined in the +[Stateless OpenPGP Protocol specification](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/). +::: + +At the time of writing (the latest revision of the SOP spec is 06), only `generate-key` and `encrypt` accept profiles. \ No newline at end of file diff --git a/docs/source/quickstart.md b/docs/source/quickstart.md new file mode 100644 index 00000000..7cfee56c --- /dev/null +++ b/docs/source/quickstart.md @@ -0,0 +1,20 @@ +# Quickstart Guide + +In this guide, we will get you started with OpenPGP using PGPainless as quickly as possible. + +At first though, you need to decide which API you want to use; + +* PGPainless' core API is powerful and heavily customizable +* The SOP API is a bit less powerful, but *dead* simple to use + +The SOP API is the recommended way to go if you just want to get started already. + +In case you need more technical documentation, Javadoc can be found in the following places: +* For the core API: {{ '[pgpainless-core](https://javadoc.io/doc/org.pgpainless/pgpainless-core/{}/index.html)'.format(env.config.version) }} +* For the SOP API: {{ '[pgpainless-sop](https://javadoc.io/doc/org.pgpainless/pgpainless-sop/{}/index.html)'.format(env.config.version) }} + +```{include} pgpainless-sop/quickstart.md +``` + +```{include} pgpainless-core/quickstart.md +``` \ No newline at end of file diff --git a/docs/source/sop.md b/docs/source/sop.md new file mode 100644 index 00000000..d092f50b --- /dev/null +++ b/docs/source/sop.md @@ -0,0 +1,10 @@ +# Stateless OpenPGP Protocol (SOP) + +The [Stateless OpenPGP Protocol](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/) +(short *SOP*) is a specification of a standardized command line interface for a limited set of OpenPGP operations. + +By standardizing the interface, users are able to choose between different, compatible implementations. + +:::{note} +This chapter is work in progress. +::: \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a488f210..0d184210 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.4-rc-1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/misc/OpenPGPMessageFormat.md b/misc/OpenPGPMessageFormat.md new file mode 100644 index 00000000..83217b7d --- /dev/null +++ b/misc/OpenPGPMessageFormat.md @@ -0,0 +1,110 @@ + + +# Pushdown Automaton for the OpenPGP Message Format + +See [RFC4880 §11.3. OpenPGP Messages](https://www.rfc-editor.org/rfc/rfc4880#section-11.3) for the formal definition. + +A simulation of the automaton can be found [here](https://automatonsimulator.com/#%7B%22type%22%3A%22PDA%22%2C%22pda%22%3A%7B%22transitions%22%3A%7B%22start%22%3A%7B%22%22%3A%7B%22%22%3A%5B%7B%22state%22%3A%22s12%22%2C%22stackPushChar%22%3A%22%23%22%7D%5D%2C%22%23%22%3A%5B%5D%7D%7D%2C%22s0%22%3A%7B%22C%22%3A%7B%22M%22%3A%5B%7B%22state%22%3A%22s0%22%2C%22stackPushChar%22%3A%22M%22%7D%5D%7D%2C%22L%22%3A%7B%22M%22%3A%5B%7B%22state%22%3A%22s1%22%2C%22stackPushChar%22%3A%22%22%7D%5D%7D%2C%22S%22%3A%7B%22M%22%3A%5B%7B%22state%22%3A%22s0%22%2C%22stackPushChar%22%3A%22M%22%7D%5D%2C%22o%22%3A%5B%5D%7D%2C%22O%22%3A%7B%22M%22%3A%5B%7B%22state%22%3A%22s9%22%2C%22stackPushChar%22%3A%22o%22%7D%5D%7D%2C%22E%22%3A%7B%22M%22%3A%5B%5D%7D%2C%22p%22%3A%7B%22M%22%3A%5B%7B%22state%22%3A%22s6%22%2C%22stackPushChar%22%3A%22X%22%7D%5D%7D%2C%22s%22%3A%7B%22M%22%3A%5B%7B%22state%22%3A%22s6%22%2C%22stackPushChar%22%3A%22X%22%7D%5D%7D%2C%22I%22%3A%7B%22M%22%3A%5B%7B%22state%22%3A%22s8%22%2C%22stackPushChar%22%3A%22E%22%7D%5D%7D%2C%22J%22%3A%7B%22M%22%3A%5B%7B%22state%22%3A%22s8%22%2C%22stackPushChar%22%3A%22E%22%7D%5D%7D%7D%2C%22s1%22%3A%7B%22%22%3A%7B%22%22%3A%5B%5D%2C%22%23%22%3A%5B%7B%22state%22%3A%22s4%22%2C%22stackPushChar%22%3A%22%22%7D%5D%7D%2C%22S%22%3A%7B%22o%22%3A%5B%7B%22state%22%3A%22s10%22%2C%22stackPushChar%22%3A%22%22%7D%5D%7D%7D%2C%22s6%22%3A%7B%22p%22%3A%7B%22X%22%3A%5B%7B%22state%22%3A%22s6%22%2C%22stackPushChar%22%3A%22X%22%7D%5D%7D%2C%22s%22%3A%7B%22X%22%3A%5B%7B%22state%22%3A%22s6%22%2C%22stackPushChar%22%3A%22X%22%7D%5D%7D%2C%22I%22%3A%7B%22X%22%3A%5B%7B%22state%22%3A%22s8%22%2C%22stackPushChar%22%3A%22E%22%7D%5D%7D%2C%22J%22%3A%7B%22X%22%3A%5B%7B%22state%22%3A%22s8%22%2C%22stackPushChar%22%3A%22E%22%7D%5D%7D%7D%2C%22s8%22%3A%7B%22%22%3A%7B%22E%22%3A%5B%7B%22state%22%3A%22s0%22%2C%22stackPushChar%22%3A%22M%22%7D%5D%7D%7D%2C%22s9%22%3A%7B%22%22%3A%7B%22%22%3A%5B%7B%22state%22%3A%22s0%22%2C%22stackPushChar%22%3A%22M%22%7D%5D%7D%7D%2C%22s10%22%3A%7B%22%22%3A%7B%22%22%3A%5B%5D%2C%22%23%22%3A%5B%7B%22state%22%3A%22s4%22%2C%22stackPushChar%22%3A%22%22%7D%5D%7D%2C%22S%22%3A%7B%22o%22%3A%5B%7B%22state%22%3A%22s10%22%2C%22stackPushChar%22%3A%22%22%7D%5D%7D%7D%2C%22s4%22%3A%7B%22%22%3A%7B%22o%22%3A%5B%5D%7D%7D%2C%22s12%22%3A%7B%22%22%3A%7B%22%22%3A%5B%7B%22state%22%3A%22s0%22%2C%22stackPushChar%22%3A%22M%22%7D%5D%7D%7D%7D%2C%22startState%22%3A%22start%22%2C%22acceptStates%22%3A%5B%22s4%22%5D%7D%2C%22states%22%3A%7B%22start%22%3A%7B%7D%2C%22s12%22%3A%7B%22top%22%3A395.00001525878906%2C%22left%22%3A99%2C%22displayId%22%3A%22Add%20Terminal%22%7D%2C%22s0%22%3A%7B%22top%22%3A259.00001525878906%2C%22left%22%3A162%2C%22displayId%22%3A%22OpenPGP%20Message%22%7D%2C%22s1%22%3A%7B%22top%22%3A304.00001525878906%2C%22left%22%3A524%2C%22displayId%22%3A%22Literal%20Message%22%7D%2C%22s9%22%3A%7B%22top%22%3A476.00001525878906%2C%22left%22%3A282%2C%22displayId%22%3A%22One%20Pass%20Signatures%22%7D%2C%22s6%22%3A%7B%22top%22%3A100%2C%22left%22%3A324%2C%22displayId%22%3A%22ESKs%22%7D%2C%22s8%22%3A%7B%22top%22%3A202%2C%22left%22%3A471%2C%22displayId%22%3A%22Encrypted%20Data%22%7D%2C%22s4%22%3A%7B%22isAccept%22%3Atrue%2C%22top%22%3A381.00001525878906%2C%22left%22%3A832%2C%22displayId%22%3A%22Accept%22%7D%2C%22s10%22%3A%7B%22top%22%3A237.00001525878906%2C%22left%22%3A809%2C%22displayId%22%3A%22Corresponding%20Signatures%22%7D%7D%2C%22transitions%22%3A%5B%7B%22stateA%22%3A%22start%22%2C%22label%22%3A%22%CF%B5%2C%CF%B5%2C%23%22%2C%22stateB%22%3A%22s12%22%7D%2C%7B%22stateA%22%3A%22s0%22%2C%22label%22%3A%22C%2CM%2CM%22%2C%22stateB%22%3A%22s0%22%7D%2C%7B%22stateA%22%3A%22s0%22%2C%22label%22%3A%22L%2CM%2C%CF%B5%22%2C%22stateB%22%3A%22s1%22%7D%2C%7B%22stateA%22%3A%22s0%22%2C%22label%22%3A%22S%2CM%2CM%22%2C%22stateB%22%3A%22s0%22%7D%2C%7B%22stateA%22%3A%22s0%22%2C%22label%22%3A%22O%2CM%2Co%22%2C%22stateB%22%3A%22s9%22%7D%2C%7B%22stateA%22%3A%22s0%22%2C%22label%22%3A%22p%2CM%2CX%22%2C%22stateB%22%3A%22s6%22%7D%2C%7B%22stateA%22%3A%22s0%22%2C%22label%22%3A%22s%2CM%2CX%22%2C%22stateB%22%3A%22s6%22%7D%2C%7B%22stateA%22%3A%22s0%22%2C%22label%22%3A%22I%2CM%2CE%22%2C%22stateB%22%3A%22s8%22%7D%2C%7B%22stateA%22%3A%22s0%22%2C%22label%22%3A%22J%2CM%2CE%22%2C%22stateB%22%3A%22s8%22%7D%2C%7B%22stateA%22%3A%22s1%22%2C%22label%22%3A%22%CF%B5%2C%23%2C%CF%B5%22%2C%22stateB%22%3A%22s4%22%7D%2C%7B%22stateA%22%3A%22s1%22%2C%22label%22%3A%22S%2Co%2C%CF%B5%22%2C%22stateB%22%3A%22s10%22%7D%2C%7B%22stateA%22%3A%22s6%22%2C%22label%22%3A%22p%2CX%2CX%22%2C%22stateB%22%3A%22s6%22%7D%2C%7B%22stateA%22%3A%22s6%22%2C%22label%22%3A%22s%2CX%2CX%22%2C%22stateB%22%3A%22s6%22%7D%2C%7B%22stateA%22%3A%22s6%22%2C%22label%22%3A%22I%2CX%2CE%22%2C%22stateB%22%3A%22s8%22%7D%2C%7B%22stateA%22%3A%22s6%22%2C%22label%22%3A%22J%2CX%2CE%22%2C%22stateB%22%3A%22s8%22%7D%2C%7B%22stateA%22%3A%22s8%22%2C%22label%22%3A%22%CF%B5%2CE%2CM%22%2C%22stateB%22%3A%22s0%22%7D%2C%7B%22stateA%22%3A%22s9%22%2C%22label%22%3A%22%CF%B5%2C%CF%B5%2CM%22%2C%22stateB%22%3A%22s0%22%7D%2C%7B%22stateA%22%3A%22s10%22%2C%22label%22%3A%22%CF%B5%2C%23%2C%CF%B5%22%2C%22stateB%22%3A%22s4%22%7D%2C%7B%22stateA%22%3A%22s10%22%2C%22label%22%3A%22S%2Co%2C%CF%B5%22%2C%22stateB%22%3A%22s10%22%7D%2C%7B%22stateA%22%3A%22s12%22%2C%22label%22%3A%22%CF%B5%2C%CF%B5%2CM%22%2C%22stateB%22%3A%22s0%22%7D%5D%2C%22bulkTests%22%3A%7B%22accept%22%3A%22L%5CnCL%5CnCCL%5CnSL%5CnSSL%5CnSCL%5CnSpICL%5CnOLS%5CnOOLSS%5CnCspIL%5CnppppJCOLS%5CnOspILS%5CnOpJCLS%5CnOCLS%5CnCOCLS%22%2C%22reject%22%3A%22C%5CnO%5CnOL%5CnLS%5CnLL%5CnS%5Cns%5Cnp%5CnOOLS%22%7D%7D). + +RFC4880 defines the grammar of OpenPGP messages as follows: +``` + OpenPGP Message :- Encrypted Message | Signed Message | + Compressed Message | Literal Message. + + Compressed Message :- Compressed Data Packet. + + Literal Message :- Literal Data Packet. + + ESK :- Public-Key Encrypted Session Key Packet | + Symmetric-Key Encrypted Session Key Packet. + + ESK Sequence :- ESK | ESK Sequence, ESK. + + Encrypted Data :- Symmetrically Encrypted Data Packet | + Symmetrically Encrypted Integrity Protected Data Packet + + Encrypted Message :- Encrypted Data | ESK Sequence, Encrypted Data. + + One-Pass Signed Message :- One-Pass Signature Packet, + OpenPGP Message, Corresponding Signature Packet. + + Signed Message :- Signature Packet, OpenPGP Message | + One-Pass Signed Message. + + In addition, decrypting a Symmetrically Encrypted Data packet or a + Symmetrically Encrypted Integrity Protected Data packet as well as + decompressing a Compressed Data packet must yield a valid OpenPGP + Message. +``` + +This grammar can be translated into a [pushdown automaton](https://en.wikipedia.org/wiki/Pushdown_automaton) with +the following graphical representation: + +```mermaid +graph LR + start((start)) -- "ε,ε/m#" --> pgpmsg((OpenPGP Message)) + pgpmsg -- "Literal Data,m/ε" --> literal((Literal Message)) + literal -- "ε,#/ε" --> accept((Valid)) + literal -- "Signature,o/ε" --> sig4ops((Corresponding Signature)) + sig4ops -- "Signature,o/ε" --> sig4ops + sig4ops -- "ε,#/ε" --> accept + pgpmsg -- "OnePassSignature,m/o" --> ops((One-Pass-Signed Message)) + ops -- "ε,ε/m" --> pgpmsg + pgpmsg -- "Signature,m/ε" --> signed((Signed Message)) + signed -- "ε,ε/m" --> pgpmsg + pgpmsg -- "Compressed Data,m/ε" --> comp((Compressed Message)) + comp -. "ε,ε/m" .-> pgpmsg + comp -- "ε,#/ε" --> accept + comp -- "Signature,o/ε" --> sig4ops + pgpmsg -- "SKESK|PKESK,m/k" --> esks((ESKs)) + pgpmsg -- "Sym. Enc. (Int. Prot.) Data,m/ε" --> enc + esks -- "SKESK|PKESK,k/k" --> esks + esks -- "Sym. Enc. (Int. Prot.) Data,k/ε" --> enc((Encrypted Message)) + enc -. "ε,ε/m" .-> pgpmsg + enc -- "ε,#/ε" --> accept + enc -- "Signature,o/ε" --> sig4ops + accept -- "ε,ε/ε" --> accept +``` + +Formally, the PDA is defined as $M = (\mathcal{Q}, \Sigma, \Upgamma, \delta, q_0, Z, F)$, where +* $\mathcal{Q}$ is a finite set of states +* $\Sigma$ is a finite set which is called the input alphabet +* $\Upgamma$ is a finite set which is called the stack alphabet +* $\delta$ is a finite set of $\mathcal{Q}\times(\Sigma\cup\textbraceleft\epsilon\textbraceright)\times\Upgamma\times\mathcal{Q}\times\Upgamma^*$, the transition relation +* $q_0\in\mathcal{Q}$ is the start state +* $Z\in\Upgamma$ is the initial stack symbol +* $F\subseteq\mathcal{Q}$ is the set of accepting states + +In our diagram, the initial state is $q_0 = \text{start}$. +The initial stack symbol is $Z=\epsilon$ (TODO: Make it `#`?). +The set of accepting states is $F=\textbraceleft\text{valid}\textbraceright$. +$\delta$ is defined by the transitions shown in the graph diagram. + +The input alphabet $\Sigma$ consists of the following OpenPGP packets: +* $\text{Literal Data}$: Literal Data Packet +* $\text{Signature}$: Signature Packet +* $\text{OnePassSignature}$: One-Pass-Signature Packet +* $\text{Compressed Data}$: Compressed Data Packet +* $\text{SKESK}$: Symmetric-Key Encrypted Session Key Packet +* $\text{PKESK}$: Public-Key Encrypted Session Key Packet +* $\text{Sym. Enc. Data}$: Symmetrically Encrypted Data Packet +* $\text{Sym. Enc. Int. Prot. Data}$: Symmetrically Encrypted Integrity Protected Data Packet + +Additionally, $\epsilon$ is used to transition without reading OpenPGP packets or popping the stack. + +The following stack alphabet $\Upgamma$ is used: +* $m$: OpenPGP Message +* $o$: One-Pass-Signature packet. +* $k$: Encrypted Session Key +* `#`: Terminal for valid OpenPGP messages + +Note: The standards document states, that Marker Packets shall be ignored as well. +For the sake of readability, those transitions are omitted here. + +The dotted line indicates a nested transition. +For example, the transition $(\text{Compressed Message}, \epsilon, \epsilon, \text{OpenPGP Message}, m)$ indicates, that the content of the +Compressed Data packet itself is an OpenPGP Message. diff --git a/pgpainless-cli/README.md b/pgpainless-cli/README.md index 4f691f56..3cf0c97f 100644 --- a/pgpainless-cli/README.md +++ b/pgpainless-cli/README.md @@ -6,19 +6,21 @@ SPDX-License-Identifier: Apache-2.0 # PGPainless-CLI -PGPainless-CLI is an implementation of the [Stateless OpenPGP Command Line Interface](https://tools.ietf.org/html/draft-dkg-openpgp-stateless-cli-01) specification based on PGPainless. +[![javadoc](https://javadoc.io/badge2/org.pgpainless/pgpainless-cli/javadoc.svg)](https://javadoc.io/doc/org.pgpainless/pgpainless-cli) + +PGPainless-CLI is an implementation of the [Stateless OpenPGP Command Line Interface](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/) specification based on PGPainless. It plugs `pgpainless-sop` into `sop-java-picocli`. ## Build -To build an executable, `gradle jar` should be sufficient. The resulting jar file can be found in `pgpainless-sop/build/libs/`. +To build an executable, `gradle shadowJar` should be sufficient. The resulting jar file can be found in `pgpainless-cli/build/libs/`. ## Execute The jar file produced in the step above is executable as is. ``` -java -jar pgpainless-cli-XXX.jar help +java -jar pgpainless-cli-XXX-all.jar help ``` Alternatively you can use the provided `./pgpainless-cli` script to directly build and execute PGPainless' Stateless Command Line Interface from within Gradle. diff --git a/pgpainless-cli/build.gradle b/pgpainless-cli/build.gradle index 4c892360..e4c3f060 100644 --- a/pgpainless-cli/build.gradle +++ b/pgpainless-cli/build.gradle @@ -4,26 +4,13 @@ plugins { id 'application' -} -def generatedVersionDir = "${buildDir}/generated-version" - -sourceSets { - main { - output.dir(generatedVersionDir, builtBy: 'generateVersionProperties') - } + id 'org.graalvm.buildtools.native' version '0.10.6' + id 'com.gradleup.shadow' version '8.3.6' } -task generateVersionProperties { - doLast { - def propertiesFile = file "$generatedVersionDir/version.properties" - propertiesFile.parentFile.mkdirs() - propertiesFile.createNewFile() - // Instead of using a Properties object here, we directly write to the file - // since Properties adds a timestamp, ruining reproducibility - propertiesFile.write("version="+rootProject.version.toString()) - } +graalvmNative { + toolchainDetection = true } -processResources.dependsOn generateVersionProperties dependencies { @@ -31,18 +18,15 @@ dependencies { testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion" testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion" - // https://todd.ginsberg.com/post/testing-system-exit/ - testImplementation 'com.ginsberg:junit5-system-exit:1.1.1' - // We want logback logging in tests - testImplementation 'ch.qos.logback:logback-classic:1.2.5' + + // implementation "ch.qos.logback:logback-core:1.2.6" + // We want logback logging in tests and in the app + testImplementation "ch.qos.logback:logback-classic:$logbackVersion" + // implementation "ch.qos.logback:logback-classic:$logbackVersion" + implementation "org.slf4j:slf4j-nop:$slf4jVersion" implementation(project(":pgpainless-sop")) - implementation(project(":sop-java")) - implementation(project(":sop-java-picocli")) - - implementation "info.picocli:picocli:$picocliVersion" - // We don't want logging in the application itself - implementation "org.slf4j:slf4j-nop:$slf4jVersion" + implementation "org.pgpainless:sop-java-picocli:$sopJavaVersion" // https://mvnrepository.com/artifact/com.google.code.findbugs/jsr305 implementation group: 'com.google.code.findbugs', name: 'jsr305', version: '3.0.2' @@ -50,18 +34,8 @@ dependencies { mainClassName = 'org.pgpainless.cli.PGPainlessCLI' -jar { - manifest { - attributes 'Main-Class': "$mainClassName" - } - - from { - configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } - } { - exclude "META-INF/*.SF" - exclude "META-INF/*.DSA" - exclude "META-INF/*.RSA" - } +application { + mainClass = mainClassName } run { @@ -72,5 +46,3 @@ run { args Eval.me(appArgs) } } - -tasks."jar".dependsOn(":pgpainless-core:assemble") diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-armor.1 b/pgpainless-cli/packaging/man/pgpainless-cli-armor.1 new file mode 100644 index 00000000..0d948dfd --- /dev/null +++ b/pgpainless-cli/packaging/man/pgpainless-cli-armor.1 @@ -0,0 +1,41 @@ +'\" t +.\" Title: pgpainless-cli-armor +.\" Author: [see the "AUTHOR(S)" section] +.\" Generator: Asciidoctor 2.0.10 +.\" Manual: PGPainless-CLI Manual +.\" Source: +.\" Language: English +.\" +.TH "PGPAINLESS\-CLI\-ARMOR" "1" "" "" "PGPainless\-CLI Manual" +.ie \n(.g .ds Aq \(aq +.el .ds Aq ' +.ss \n[.ss] 0 +.nh +.ad l +.de URL +\fI\\$2\fP <\\$1>\\$3 +.. +.als MTO URL +.if \n[.g] \{\ +. mso www.tmac +. am URL +. ad l +. . +. am MTO +. ad l +. . +. LINKSTYLE blue R < > +.\} +.SH "NAME" +pgpainless\-cli\-armor \- Add ASCII Armor to standard input +.SH "SYNOPSIS" +.sp +\fBpgpainless\-cli armor\fP [\fB\-\-stacktrace\fP] +.SH "DESCRIPTION" + +.SH "OPTIONS" +.sp +\fB\-\-stacktrace\fP +.RS 4 +Print stacktrace +.RE \ No newline at end of file diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-change-key-password.1 b/pgpainless-cli/packaging/man/pgpainless-cli-change-key-password.1 new file mode 100644 index 00000000..172f8280 --- /dev/null +++ b/pgpainless-cli/packaging/man/pgpainless-cli-change-key-password.1 @@ -0,0 +1,67 @@ +'\" t +.\" Title: pgpainless-cli-change-key-password +.\" Author: [see the "AUTHOR(S)" section] +.\" Generator: Asciidoctor 2.0.10 +.\" Manual: PGPainless-CLI Manual +.\" Source: +.\" Language: English +.\" +.TH "PGPAINLESS\-CLI\-CHANGE\-KEY\-PASSWORD" "1" "" "" "PGPainless\-CLI Manual" +.ie \n(.g .ds Aq \(aq +.el .ds Aq ' +.ss \n[.ss] 0 +.nh +.ad l +.de URL +\fI\\$2\fP <\\$1>\\$3 +.. +.als MTO URL +.if \n[.g] \{\ +. mso www.tmac +. am URL +. ad l +. . +. am MTO +. ad l +. . +. LINKSTYLE blue R < > +.\} +.SH "NAME" +pgpainless\-cli\-change\-key\-password \- Update the password of a key +.SH "SYNOPSIS" +.sp +\fBpgpainless\-cli change\-key\-password\fP [\fB\-\-[no\-]armor\fP] [\fB\-\-stacktrace\fP] [\fB\-\-new\-key\-password\fP +[=\fIPASSWORD\fP]] [\fB\-\-old\-key\-password\fP=\fIPASSWORD\fP]... +.SH "DESCRIPTION" +.sp +Unlock all secret keys from STDIN using the given old passwords and emit them re\-locked using the new password to STDOUT. +If any (sub\-) key cannot be unlocked, this operation will exit with error code 67. +.SH "OPTIONS" +.sp +\fB\-\-new\-key\-password\fP[=\fIPASSWORD\fP] +.RS 4 +New password to lock the keys with. +.sp +If no new password is passed in, the keys will be emitted unlocked. +.sp +Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). +.RE +.sp +\fB\-\-[no\-]armor\fP +.RS 4 +ASCII armor the output +.RE +.sp +\fB\-\-old\-key\-password\fP=\fIPASSWORD\fP +.RS 4 +Old passwords to unlock the keys with. +.sp +Multiple passwords can be passed in, which are tested sequentially to unlock locked subkeys. +.sp +Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). +.RE +.sp +\fB\-\-stacktrace\fP +.RS 4 +Print stacktrace +.RE \ No newline at end of file diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-dearmor.1 b/pgpainless-cli/packaging/man/pgpainless-cli-dearmor.1 new file mode 100644 index 00000000..85f9277d --- /dev/null +++ b/pgpainless-cli/packaging/man/pgpainless-cli-dearmor.1 @@ -0,0 +1,41 @@ +'\" t +.\" Title: pgpainless-cli-dearmor +.\" Author: [see the "AUTHOR(S)" section] +.\" Generator: Asciidoctor 2.0.10 +.\" Manual: PGPainless-CLI Manual +.\" Source: +.\" Language: English +.\" +.TH "PGPAINLESS\-CLI\-DEARMOR" "1" "" "" "PGPainless\-CLI Manual" +.ie \n(.g .ds Aq \(aq +.el .ds Aq ' +.ss \n[.ss] 0 +.nh +.ad l +.de URL +\fI\\$2\fP <\\$1>\\$3 +.. +.als MTO URL +.if \n[.g] \{\ +. mso www.tmac +. am URL +. ad l +. . +. am MTO +. ad l +. . +. LINKSTYLE blue R < > +.\} +.SH "NAME" +pgpainless\-cli\-dearmor \- Remove ASCII Armor from standard input +.SH "SYNOPSIS" +.sp +\fBpgpainless\-cli dearmor\fP [\fB\-\-stacktrace\fP] +.SH "DESCRIPTION" + +.SH "OPTIONS" +.sp +\fB\-\-stacktrace\fP +.RS 4 +Print stacktrace +.RE \ No newline at end of file diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-decrypt.1 b/pgpainless-cli/packaging/man/pgpainless-cli-decrypt.1 new file mode 100644 index 00000000..eb843e18 --- /dev/null +++ b/pgpainless-cli/packaging/man/pgpainless-cli-decrypt.1 @@ -0,0 +1,106 @@ +'\" t +.\" Title: pgpainless-cli-decrypt +.\" Author: [see the "AUTHOR(S)" section] +.\" Generator: Asciidoctor 2.0.10 +.\" Manual: PGPainless-CLI Manual +.\" Source: +.\" Language: English +.\" +.TH "PGPAINLESS\-CLI\-DECRYPT" "1" "" "" "PGPainless\-CLI Manual" +.ie \n(.g .ds Aq \(aq +.el .ds Aq ' +.ss \n[.ss] 0 +.nh +.ad l +.de URL +\fI\\$2\fP <\\$1>\\$3 +.. +.als MTO URL +.if \n[.g] \{\ +. mso www.tmac +. am URL +. ad l +. . +. am MTO +. ad l +. . +. LINKSTYLE blue R < > +.\} +.SH "NAME" +pgpainless\-cli\-decrypt \- Decrypt a message +.SH "SYNOPSIS" +.sp +\fBpgpainless\-cli decrypt\fP [\fB\-\-stacktrace\fP] [\fB\-\-session\-key\-out\fP=\fISESSIONKEY\fP] +[\fB\-\-verifications\-out\fP=\fIVERIFICATIONS\fP] [\fB\-\-verify\-not\-after\fP=\fIDATE\fP] +[\fB\-\-verify\-not\-before\fP=\fIDATE\fP] [\fB\-\-verify\-with\fP=\fICERT\fP]... +[\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP]... [\fB\-\-with\-password\fP=\fIPASSWORD\fP]... +[\fB\-\-with\-session\-key\fP=\fISESSIONKEY\fP]... [\fIKEY\fP...] +.SH "DESCRIPTION" + +.SH "OPTIONS" +.sp +\fB\-\-session\-key\-out\fP=\fISESSIONKEY\fP +.RS 4 +Can be used to learn the session key on successful decryption +.RE +.sp +\fB\-\-stacktrace\fP +.RS 4 +Print stacktrace +.RE +.sp +\fB\-\-verifications\-out\fP=\fIVERIFICATIONS\fP, \fB\-\-verify\-not\-after\fP=\fIDATE\fP +.RS 4 +ISO\-8601 formatted UTC date (e.g. \(aq2020\-11\-23T16:35Z) +.sp +Reject signatures with a creation date not in range. +.sp +Defaults to current system time (\(aqnow\(aq). +.sp +Accepts special value \(aq\-\(aq for end of time. +.RE +.sp +\fB\-\-verify\-not\-before\fP=\fIDATE\fP +.RS 4 +ISO\-8601 formatted UTC date (e.g. \(aq2020\-11\-23T16:35Z) +.sp +Reject signatures with a creation date not in range. +.sp +Defaults to beginning of time (\(aq\-\(aq). +.RE +.sp +\fB\-\-verify\-with\fP=\fICERT\fP +.RS 4 +Certificates for signature verification +.RE +.sp +\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP +.RS 4 +Passphrase to unlock the secret key(s). +.sp +Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). +.RE +.sp +\fB\-\-with\-password\fP=\fIPASSWORD\fP +.RS 4 +Symmetric passphrase to decrypt the message with. +.sp +Enables decryption based on any "SKESK" packets in the "CIPHERTEXT". +.sp +Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). +.RE +.sp +\fB\-\-with\-session\-key\fP=\fISESSIONKEY\fP +.RS 4 +Symmetric message key (session key). +.sp +Enables decryption of the "CIPHERTEXT" using the session key directly against the "SEIPD" packet. +.sp +Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). +.RE +.SH "ARGUMENTS" +.sp +[\fIKEY\fP...] +.RS 4 +Secret keys to attempt decryption with +.RE \ No newline at end of file diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-encrypt.1 b/pgpainless-cli/packaging/man/pgpainless-cli-encrypt.1 new file mode 100644 index 00000000..3cf7aefc --- /dev/null +++ b/pgpainless-cli/packaging/man/pgpainless-cli-encrypt.1 @@ -0,0 +1,84 @@ +'\" t +.\" Title: pgpainless-cli-encrypt +.\" Author: [see the "AUTHOR(S)" section] +.\" Generator: Asciidoctor 2.0.10 +.\" Manual: PGPainless-CLI Manual +.\" Source: +.\" Language: English +.\" +.TH "PGPAINLESS\-CLI\-ENCRYPT" "1" "" "" "PGPainless\-CLI Manual" +.ie \n(.g .ds Aq \(aq +.el .ds Aq ' +.ss \n[.ss] 0 +.nh +.ad l +.de URL +\fI\\$2\fP <\\$1>\\$3 +.. +.als MTO URL +.if \n[.g] \{\ +. mso www.tmac +. am URL +. ad l +. . +. am MTO +. ad l +. . +. LINKSTYLE blue R < > +.\} +.SH "NAME" +pgpainless\-cli\-encrypt \- Encrypt a message from standard input +.SH "SYNOPSIS" +.sp +\fBpgpainless\-cli encrypt\fP [\fB\-\-[no\-]armor\fP] [\fB\-\-stacktrace\fP] [\fB\-\-as\fP=\fI{binary|text}\fP] +[\fB\-\-profile\fP=\fIPROFILE\fP] [\fB\-\-session\-key\-out\fP=\fISESSIONKEY\fP] +[\fB\-\-sign\-with\fP=\fIKEY\fP]... [\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP]... +[\fB\-\-with\-password\fP=\fIPASSWORD\fP]... [\fICERTS\fP...] +.SH "DESCRIPTION" + +.SH "OPTIONS" +.sp +\fB\-\-as\fP=\fI{binary|text}\fP +.RS 4 +Type of the input data. Defaults to \(aqbinary\(aq +.RE +.sp +\fB\-\-[no\-]armor\fP +.RS 4 +ASCII armor the output +.RE +.sp +\fB\-\-profile\fP=\fIPROFILE\fP +.RS 4 +Profile identifier to switch between profiles +.RE +.sp +\fB\-\-session\-key\-out\fP=\fISESSIONKEY\fP, \fB\-\-sign\-with\fP=\fIKEY\fP +.RS 4 +Sign the output with a private key +.RE +.sp +\fB\-\-stacktrace\fP +.RS 4 +Print stacktrace +.RE +.sp +\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP +.RS 4 +Passphrase to unlock the secret key(s). +.sp +Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). +.RE +.sp +\fB\-\-with\-password\fP=\fIPASSWORD\fP +.RS 4 +Encrypt the message with a password. +.sp +Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). +.RE +.SH "ARGUMENTS" +.sp +[\fICERTS\fP...] +.RS 4 +Certificates the message gets encrypted to +.RE \ No newline at end of file diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-extract-cert.1 b/pgpainless-cli/packaging/man/pgpainless-cli-extract-cert.1 new file mode 100644 index 00000000..db8f7619 --- /dev/null +++ b/pgpainless-cli/packaging/man/pgpainless-cli-extract-cert.1 @@ -0,0 +1,47 @@ +'\" t +.\" Title: pgpainless-cli-extract-cert +.\" Author: [see the "AUTHOR(S)" section] +.\" Generator: Asciidoctor 2.0.10 +.\" Manual: PGPainless-CLI Manual +.\" Source: +.\" Language: English +.\" +.TH "PGPAINLESS\-CLI\-EXTRACT\-CERT" "1" "" "" "PGPainless\-CLI Manual" +.ie \n(.g .ds Aq \(aq +.el .ds Aq ' +.ss \n[.ss] 0 +.nh +.ad l +.de URL +\fI\\$2\fP <\\$1>\\$3 +.. +.als MTO URL +.if \n[.g] \{\ +. mso www.tmac +. am URL +. ad l +. . +. am MTO +. ad l +. . +. LINKSTYLE blue R < > +.\} +.SH "NAME" +pgpainless\-cli\-extract\-cert \- Extract a public key certificate from a secret key +.SH "SYNOPSIS" +.sp +\fBpgpainless\-cli extract\-cert\fP [\fB\-\-[no\-]armor\fP] [\fB\-\-stacktrace\fP] +.SH "DESCRIPTION" +.sp +Read a secret key from STDIN and emit the public key certificate to STDOUT. +.SH "OPTIONS" +.sp +\fB\-\-[no\-]armor\fP +.RS 4 +ASCII armor the output +.RE +.sp +\fB\-\-stacktrace\fP +.RS 4 +Print stacktrace +.RE \ No newline at end of file diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-generate-completion.1 b/pgpainless-cli/packaging/man/pgpainless-cli-generate-completion.1 new file mode 100644 index 00000000..5c50ee96 --- /dev/null +++ b/pgpainless-cli/packaging/man/pgpainless-cli-generate-completion.1 @@ -0,0 +1,165 @@ +'\" t +.\" Title: pgpainless-cli-generate-completion +.\" Author: [see the "AUTHOR(S)" section] +.\" Generator: Asciidoctor 2.0.10 +.\" Manual: PGPainless-CLI Manual +.\" Source: generate-completion 4.6.3 +.\" Language: English +.\" +.TH "PGPAINLESS\-CLI\-GENERATE\-COMPLETION" "1" "" "generate\-completion 4.6.3" "PGPainless\-CLI Manual" +.ie \n(.g .ds Aq \(aq +.el .ds Aq ' +.ss \n[.ss] 0 +.nh +.ad l +.de URL +\fI\\$2\fP <\\$1>\\$3 +.. +.als MTO URL +.if \n[.g] \{\ +. mso www.tmac +. am URL +. ad l +. . +. am MTO +. ad l +. . +. LINKSTYLE blue R < > +.\} +.SH "NAME" +pgpainless\-cli\-generate\-completion \- Stateless OpenPGP Protocol +.SH "SYNOPSIS" +.sp +\fBpgpainless\-cli generate\-completion\fP [\fB\-hV\fP] [\fB\-\-stacktrace\fP] +.SH "DESCRIPTION" +.sp +Generate bash/zsh completion script for pgpainless\-cli. +Run the following command to give \f(CRpgpainless\-cli\fP TAB completion in the current shell: +.sp +.if n .RS 4 +.nf +source <(pgpainless\-cli generate\-completion) +.fi +.if n .RE +.SH "OPTIONS" +.sp +\fB\-h\fP, \fB\-\-help\fP +.RS 4 +Show this help message and exit. +.RE +.sp +\fB\-\-stacktrace\fP +.RS 4 +Print stacktrace +.RE +.sp +\fB\-V\fP, \fB\-\-version\fP +.RS 4 +Print version information and exit. +.RE +.SH "EXIT CODES:" +.sp +\fB0\fP +.RS 4 +Successful program execution +.RE +.sp +\fB1\fP +.RS 4 +Generic program error +.RE +.sp +\fB3\fP +.RS 4 +Verification requested but no verifiable signature found +.RE +.sp +\fB13\fP +.RS 4 +Unsupported asymmetric algorithm +.RE +.sp +\fB17\fP +.RS 4 +Certificate is not encryption capable +.RE +.sp +\fB19\fP +.RS 4 +Usage error: Missing argument +.RE +.sp +\fB23\fP +.RS 4 +Incomplete verification instructions +.RE +.sp +\fB29\fP +.RS 4 +Unable to decrypt +.RE +.sp +\fB31\fP +.RS 4 +Password is not human\-readable +.RE +.sp +\fB37\fP +.RS 4 +Unsupported Option +.RE +.sp +\fB41\fP +.RS 4 +Invalid data or data of wrong type encountered +.RE +.sp +\fB53\fP +.RS 4 +Non\-text input received where text was expected +.RE +.sp +\fB59\fP +.RS 4 +Output file already exists +.RE +.sp +\fB61\fP +.RS 4 +Input file does not exist +.RE +.sp +\fB67\fP +.RS 4 +Cannot unlock password protected secret key +.RE +.sp +\fB69\fP +.RS 4 +Unsupported subcommand +.RE +.sp +\fB71\fP +.RS 4 +Unsupported special prefix (e.g. "@ENV/@FD") of indirect parameter +.RE +.sp +\fB73\fP +.RS 4 +Ambiguous input (a filename matching the designator already exists) +.RE +.sp +\fB79\fP +.RS 4 +Key is not signing capable +.RE +.sp +\fB83\fP +.RS 4 +Options were supplied that are incompatible with each other +.RE +.sp +\fB89\fP +.RS 4 +The requested profile is unsupported, or the indicated subcommand does not accept profiles +.RE \ No newline at end of file diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-generate-key.1 b/pgpainless-cli/packaging/man/pgpainless-cli-generate-key.1 new file mode 100644 index 00000000..f72332fa --- /dev/null +++ b/pgpainless-cli/packaging/man/pgpainless-cli-generate-key.1 @@ -0,0 +1,71 @@ +'\" t +.\" Title: pgpainless-cli-generate-key +.\" Author: [see the "AUTHOR(S)" section] +.\" Generator: Asciidoctor 2.0.10 +.\" Manual: PGPainless-CLI Manual +.\" Source: +.\" Language: English +.\" +.TH "PGPAINLESS\-CLI\-GENERATE\-KEY" "1" "" "" "PGPainless\-CLI Manual" +.ie \n(.g .ds Aq \(aq +.el .ds Aq ' +.ss \n[.ss] 0 +.nh +.ad l +.de URL +\fI\\$2\fP <\\$1>\\$3 +.. +.als MTO URL +.if \n[.g] \{\ +. mso www.tmac +. am URL +. ad l +. . +. am MTO +. ad l +. . +. LINKSTYLE blue R < > +.\} +.SH "NAME" +pgpainless\-cli\-generate\-key \- Generate a secret key +.SH "SYNOPSIS" +.sp +\fBpgpainless\-cli generate\-key\fP [\fB\-\-[no\-]armor\fP] [\fB\-\-signing\-only\fP] [\fB\-\-stacktrace\fP] +[\fB\-\-profile\fP=\fIPROFILE\fP] [\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP] [\fIUSERID\fP...] +.SH "DESCRIPTION" + +.SH "OPTIONS" +.sp +\fB\-\-[no\-]armor\fP +.RS 4 +ASCII armor the output +.RE +.sp +\fB\-\-profile\fP=\fIPROFILE\fP +.RS 4 +Profile identifier to switch between profiles +.RE +.sp +\fB\-\-signing\-only\fP +.RS 4 +Generate a key that can only be used for signing +.RE +.sp +\fB\-\-stacktrace\fP +.RS 4 +Print stacktrace +.RE +.sp +\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP +.RS 4 +Password to protect the private key with +.sp +Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). +.RE +.SH "ARGUMENTS" +.sp +[\fIUSERID\fP...] +.RS 4 +User\-ID, e.g. "Alice <\c +.MTO "alice\(atexample.com" "" ">"" +.RE \ No newline at end of file diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-help.1 b/pgpainless-cli/packaging/man/pgpainless-cli-help.1 new file mode 100644 index 00000000..1e7c2b08 --- /dev/null +++ b/pgpainless-cli/packaging/man/pgpainless-cli-help.1 @@ -0,0 +1,160 @@ +'\" t +.\" Title: pgpainless-cli-help +.\" Author: [see the "AUTHOR(S)" section] +.\" Generator: Asciidoctor 2.0.10 +.\" Manual: PGPainless-CLI Manual +.\" Source: +.\" Language: English +.\" +.TH "PGPAINLESS\-CLI\-HELP" "1" "" "" "PGPainless\-CLI Manual" +.ie \n(.g .ds Aq \(aq +.el .ds Aq ' +.ss \n[.ss] 0 +.nh +.ad l +.de URL +\fI\\$2\fP <\\$1>\\$3 +.. +.als MTO URL +.if \n[.g] \{\ +. mso www.tmac +. am URL +. ad l +. . +. am MTO +. ad l +. . +. LINKSTYLE blue R < > +.\} +.SH "NAME" +pgpainless\-cli\-help \- Stateless OpenPGP Protocol +.SH "SYNOPSIS" +.sp +\fBpgpainless\-cli help\fP [\fB\-h\fP] [\fB\-\-stacktrace\fP] [\fICOMMAND\fP] +.SH "DESCRIPTION" +.sp +When no COMMAND is given, the usage help for the main command is displayed. +If a COMMAND is specified, the help for that command is shown. +.SH "OPTIONS" +.sp +\fB\-h\fP, \fB\-\-help\fP +.RS 4 +Show usage help for the help command and exit. +.RE +.sp +\fB\-\-stacktrace\fP +.RS 4 +Print stacktrace +.RE +.SH "ARGUMENTS" +.sp +[\fICOMMAND\fP] +.RS 4 +The COMMAND to display the usage help message for. +.RE +.SH "EXIT CODES:" +.sp +\fB0\fP +.RS 4 +Successful program execution +.RE +.sp +\fB1\fP +.RS 4 +Generic program error +.RE +.sp +\fB3\fP +.RS 4 +Verification requested but no verifiable signature found +.RE +.sp +\fB13\fP +.RS 4 +Unsupported asymmetric algorithm +.RE +.sp +\fB17\fP +.RS 4 +Certificate is not encryption capable +.RE +.sp +\fB19\fP +.RS 4 +Usage error: Missing argument +.RE +.sp +\fB23\fP +.RS 4 +Incomplete verification instructions +.RE +.sp +\fB29\fP +.RS 4 +Unable to decrypt +.RE +.sp +\fB31\fP +.RS 4 +Password is not human\-readable +.RE +.sp +\fB37\fP +.RS 4 +Unsupported Option +.RE +.sp +\fB41\fP +.RS 4 +Invalid data or data of wrong type encountered +.RE +.sp +\fB53\fP +.RS 4 +Non\-text input received where text was expected +.RE +.sp +\fB59\fP +.RS 4 +Output file already exists +.RE +.sp +\fB61\fP +.RS 4 +Input file does not exist +.RE +.sp +\fB67\fP +.RS 4 +Cannot unlock password protected secret key +.RE +.sp +\fB69\fP +.RS 4 +Unsupported subcommand +.RE +.sp +\fB71\fP +.RS 4 +Unsupported special prefix (e.g. "@ENV/@FD") of indirect parameter +.RE +.sp +\fB73\fP +.RS 4 +Ambiguous input (a filename matching the designator already exists) +.RE +.sp +\fB79\fP +.RS 4 +Key is not signing capable +.RE +.sp +\fB83\fP +.RS 4 +Options were supplied that are incompatible with each other +.RE +.sp +\fB89\fP +.RS 4 +The requested profile is unsupported, or the indicated subcommand does not accept profiles +.RE \ No newline at end of file diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-inline-detach.1 b/pgpainless-cli/packaging/man/pgpainless-cli-inline-detach.1 new file mode 100644 index 00000000..4463bedb --- /dev/null +++ b/pgpainless-cli/packaging/man/pgpainless-cli-inline-detach.1 @@ -0,0 +1,51 @@ +'\" t +.\" Title: pgpainless-cli-inline-detach +.\" Author: [see the "AUTHOR(S)" section] +.\" Generator: Asciidoctor 2.0.10 +.\" Manual: PGPainless-CLI Manual +.\" Source: +.\" Language: English +.\" +.TH "PGPAINLESS\-CLI\-INLINE\-DETACH" "1" "" "" "PGPainless\-CLI Manual" +.ie \n(.g .ds Aq \(aq +.el .ds Aq ' +.ss \n[.ss] 0 +.nh +.ad l +.de URL +\fI\\$2\fP <\\$1>\\$3 +.. +.als MTO URL +.if \n[.g] \{\ +. mso www.tmac +. am URL +. ad l +. . +. am MTO +. ad l +. . +. LINKSTYLE blue R < > +.\} +.SH "NAME" +pgpainless\-cli\-inline\-detach \- Split signatures from a clearsigned message +.SH "SYNOPSIS" +.sp +\fBpgpainless\-cli inline\-detach\fP [\fB\-\-[no\-]armor\fP] [\fB\-\-stacktrace\fP] [\fB\-\-signatures\-out\fP=\fISIGNATURES\fP] +.SH "DESCRIPTION" + +.SH "OPTIONS" +.sp +\fB\-\-[no\-]armor\fP +.RS 4 +ASCII armor the output +.RE +.sp +\fB\-\-signatures\-out\fP=\fISIGNATURES\fP +.RS 4 +Destination to which a detached signatures block will be written +.RE +.sp +\fB\-\-stacktrace\fP +.RS 4 +Print stacktrace +.RE \ No newline at end of file diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-inline-sign.1 b/pgpainless-cli/packaging/man/pgpainless-cli-inline-sign.1 new file mode 100644 index 00000000..4ee06ec5 --- /dev/null +++ b/pgpainless-cli/packaging/man/pgpainless-cli-inline-sign.1 @@ -0,0 +1,73 @@ +'\" t +.\" Title: pgpainless-cli-inline-sign +.\" Author: [see the "AUTHOR(S)" section] +.\" Generator: Asciidoctor 2.0.10 +.\" Manual: PGPainless-CLI Manual +.\" Source: +.\" Language: English +.\" +.TH "PGPAINLESS\-CLI\-INLINE\-SIGN" "1" "" "" "PGPainless\-CLI Manual" +.ie \n(.g .ds Aq \(aq +.el .ds Aq ' +.ss \n[.ss] 0 +.nh +.ad l +.de URL +\fI\\$2\fP <\\$1>\\$3 +.. +.als MTO URL +.if \n[.g] \{\ +. mso www.tmac +. am URL +. ad l +. . +. am MTO +. ad l +. . +. LINKSTYLE blue R < > +.\} +.SH "NAME" +pgpainless\-cli\-inline\-sign \- Create an inline\-signed message +.SH "SYNOPSIS" +.sp +\fBpgpainless\-cli inline\-sign\fP [\fB\-\-[no\-]armor\fP] [\fB\-\-stacktrace\fP] [\fB\-\-as\fP=\fI{binary|text|clearsigned}\fP] +[\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP]... [\fIKEYS\fP...] +.SH "DESCRIPTION" + +.SH "OPTIONS" +.sp +\fB\-\-as\fP=\fI{binary|text|clearsigned}\fP +.RS 4 +Specify the signature format of the signed message. +.sp +\(aqtext\(aq and \(aqbinary\(aq will produce inline\-signed messages. +.sp +\(aqclearsigned\(aq will make use of the cleartext signature framework. +.sp +Defaults to \(aqbinary\(aq. +.sp +If \(aq\-\-as=text\(aq and the input data is not valid UTF\-8, inline\-sign fails with return code 53. +.RE +.sp +\fB\-\-[no\-]armor\fP +.RS 4 +ASCII armor the output +.RE +.sp +\fB\-\-stacktrace\fP +.RS 4 +Print stacktrace +.RE +.sp +\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP +.RS 4 +Passphrase to unlock the secret key(s). +.sp +Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). +.RE +.SH "ARGUMENTS" +.sp +[\fIKEYS\fP...] +.RS 4 +Secret keys used for signing +.RE \ No newline at end of file diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-inline-verify.1 b/pgpainless-cli/packaging/man/pgpainless-cli-inline-verify.1 new file mode 100644 index 00000000..a2112d44 --- /dev/null +++ b/pgpainless-cli/packaging/man/pgpainless-cli-inline-verify.1 @@ -0,0 +1,73 @@ +'\" t +.\" Title: pgpainless-cli-inline-verify +.\" Author: [see the "AUTHOR(S)" section] +.\" Generator: Asciidoctor 2.0.10 +.\" Manual: PGPainless-CLI Manual +.\" Source: +.\" Language: English +.\" +.TH "PGPAINLESS\-CLI\-INLINE\-VERIFY" "1" "" "" "PGPainless\-CLI Manual" +.ie \n(.g .ds Aq \(aq +.el .ds Aq ' +.ss \n[.ss] 0 +.nh +.ad l +.de URL +\fI\\$2\fP <\\$1>\\$3 +.. +.als MTO URL +.if \n[.g] \{\ +. mso www.tmac +. am URL +. ad l +. . +. am MTO +. ad l +. . +. LINKSTYLE blue R < > +.\} +.SH "NAME" +pgpainless\-cli\-inline\-verify \- Verify an inline\-signed message +.SH "SYNOPSIS" +.sp +\fBpgpainless\-cli inline\-verify\fP [\fB\-\-stacktrace\fP] [\fB\-\-not\-after\fP=\fIDATE\fP] [\fB\-\-not\-before\fP=\fIDATE\fP] +[\fB\-\-verifications\-out\fP=\fIVERIFICATIONS\fP] [\fICERT\fP...] +.SH "DESCRIPTION" + +.SH "OPTIONS" +.sp +\fB\-\-not\-after\fP=\fIDATE\fP +.RS 4 +ISO\-8601 formatted UTC date (e.g. \(aq2020\-11\-23T16:35Z) +.sp +Reject signatures with a creation date not in range. +.sp +Defaults to current system time ("now"). +.sp +Accepts special value "\-" for end of time. +.RE +.sp +\fB\-\-not\-before\fP=\fIDATE\fP +.RS 4 +ISO\-8601 formatted UTC date (e.g. \(aq2020\-11\-23T16:35Z) +.sp +Reject signatures with a creation date not in range. +.sp +Defaults to beginning of time ("\-"). +.RE +.sp +\fB\-\-stacktrace\fP +.RS 4 +Print stacktrace +.RE +.sp +\fB\-\-verifications\-out\fP=\fIVERIFICATIONS\fP +.RS 4 +File to write details over successful verifications to +.RE +.SH "ARGUMENTS" +.sp +[\fICERT\fP...] +.RS 4 +Public key certificates for signature verification +.RE \ No newline at end of file diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-list-profiles.1 b/pgpainless-cli/packaging/man/pgpainless-cli-list-profiles.1 new file mode 100644 index 00000000..c87393a1 --- /dev/null +++ b/pgpainless-cli/packaging/man/pgpainless-cli-list-profiles.1 @@ -0,0 +1,47 @@ +'\" t +.\" Title: pgpainless-cli-list-profiles +.\" Author: [see the "AUTHOR(S)" section] +.\" Generator: Asciidoctor 2.0.10 +.\" Manual: PGPainless-CLI Manual +.\" Source: +.\" Language: English +.\" +.TH "PGPAINLESS\-CLI\-LIST\-PROFILES" "1" "" "" "PGPainless\-CLI Manual" +.ie \n(.g .ds Aq \(aq +.el .ds Aq ' +.ss \n[.ss] 0 +.nh +.ad l +.de URL +\fI\\$2\fP <\\$1>\\$3 +.. +.als MTO URL +.if \n[.g] \{\ +. mso www.tmac +. am URL +. ad l +. . +. am MTO +. ad l +. . +. LINKSTYLE blue R < > +.\} +.SH "NAME" +pgpainless\-cli\-list\-profiles \- Emit a list of profiles supported by the identified subcommand +.SH "SYNOPSIS" +.sp +\fBpgpainless\-cli list\-profiles\fP [\fB\-\-stacktrace\fP] \fICOMMAND\fP +.SH "DESCRIPTION" + +.SH "OPTIONS" +.sp +\fB\-\-stacktrace\fP +.RS 4 +Print stacktrace +.RE +.SH "ARGUMENTS" +.sp +\fICOMMAND\fP +.RS 4 +Subcommand for which to list profiles +.RE \ No newline at end of file diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-revoke-key.1 b/pgpainless-cli/packaging/man/pgpainless-cli-revoke-key.1 new file mode 100644 index 00000000..feb206f2 --- /dev/null +++ b/pgpainless-cli/packaging/man/pgpainless-cli-revoke-key.1 @@ -0,0 +1,54 @@ +'\" t +.\" Title: pgpainless-cli-revoke-key +.\" Author: [see the "AUTHOR(S)" section] +.\" Generator: Asciidoctor 2.0.10 +.\" Manual: PGPainless-CLI Manual +.\" Source: +.\" Language: English +.\" +.TH "PGPAINLESS\-CLI\-REVOKE\-KEY" "1" "" "" "PGPainless\-CLI Manual" +.ie \n(.g .ds Aq \(aq +.el .ds Aq ' +.ss \n[.ss] 0 +.nh +.ad l +.de URL +\fI\\$2\fP <\\$1>\\$3 +.. +.als MTO URL +.if \n[.g] \{\ +. mso www.tmac +. am URL +. ad l +. . +. am MTO +. ad l +. . +. LINKSTYLE blue R < > +.\} +.SH "NAME" +pgpainless\-cli\-revoke\-key \- Generate revocation certificates +.SH "SYNOPSIS" +.sp +\fBpgpainless\-cli revoke\-key\fP [\fB\-\-[no\-]armor\fP] [\fB\-\-stacktrace\fP] [\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP] +.SH "DESCRIPTION" +.sp +Emit revocation certificates for secret keys from STDIN to STDOUT. +.SH "OPTIONS" +.sp +\fB\-\-[no\-]armor\fP +.RS 4 +ASCII armor the output +.RE +.sp +\fB\-\-stacktrace\fP +.RS 4 +Print stacktrace +.RE +.sp +\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP +.RS 4 +Passphrase to unlock the secret key(s). +.sp +Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). +.RE \ No newline at end of file diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-sign.1 b/pgpainless-cli/packaging/man/pgpainless-cli-sign.1 new file mode 100644 index 00000000..d975e318 --- /dev/null +++ b/pgpainless-cli/packaging/man/pgpainless-cli-sign.1 @@ -0,0 +1,74 @@ +'\" t +.\" Title: pgpainless-cli-sign +.\" Author: [see the "AUTHOR(S)" section] +.\" Generator: Asciidoctor 2.0.10 +.\" Manual: PGPainless-CLI Manual +.\" Source: +.\" Language: English +.\" +.TH "PGPAINLESS\-CLI\-SIGN" "1" "" "" "PGPainless\-CLI Manual" +.ie \n(.g .ds Aq \(aq +.el .ds Aq ' +.ss \n[.ss] 0 +.nh +.ad l +.de URL +\fI\\$2\fP <\\$1>\\$3 +.. +.als MTO URL +.if \n[.g] \{\ +. mso www.tmac +. am URL +. ad l +. . +. am MTO +. ad l +. . +. LINKSTYLE blue R < > +.\} +.SH "NAME" +pgpainless\-cli\-sign \- Create a detached message signature +.SH "SYNOPSIS" +.sp +\fBpgpainless\-cli sign\fP [\fB\-\-[no\-]armor\fP] [\fB\-\-stacktrace\fP] [\fB\-\-as\fP=\fI{binary|text}\fP] +[\fB\-\-micalg\-out\fP=\fIMICALG\fP] [\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP]... [\fIKEYS\fP...] +.SH "DESCRIPTION" + +.SH "OPTIONS" +.sp +\fB\-\-as\fP=\fI{binary|text}\fP +.RS 4 +Specify the output format of the signed message. +.sp +Defaults to \(aqbinary\(aq. +.sp +If \(aq\-\-as=text\(aq and the input data is not valid UTF\-8, sign fails with return code 53. +.RE +.sp +\fB\-\-micalg\-out\fP=\fIMICALG\fP +.RS 4 +Emits the digest algorithm used to the specified file in a way that can be used to populate the micalg parameter for the PGP/MIME Content\-Type (RFC3156). +.RE +.sp +\fB\-\-[no\-]armor\fP +.RS 4 +ASCII armor the output +.RE +.sp +\fB\-\-stacktrace\fP +.RS 4 +Print stacktrace +.RE +.sp +\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP +.RS 4 +Passphrase to unlock the secret key(s). +.sp +Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). +.RE +.SH "ARGUMENTS" +.sp +[\fIKEYS\fP...] +.RS 4 +Secret keys used for signing +.RE \ No newline at end of file diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-verify.1 b/pgpainless-cli/packaging/man/pgpainless-cli-verify.1 new file mode 100644 index 00000000..12ede1e1 --- /dev/null +++ b/pgpainless-cli/packaging/man/pgpainless-cli-verify.1 @@ -0,0 +1,74 @@ +'\" t +.\" Title: pgpainless-cli-verify +.\" Author: [see the "AUTHOR(S)" section] +.\" Generator: Asciidoctor 2.0.10 +.\" Manual: PGPainless-CLI Manual +.\" Source: +.\" Language: English +.\" +.TH "PGPAINLESS\-CLI\-VERIFY" "1" "" "" "PGPainless\-CLI Manual" +.ie \n(.g .ds Aq \(aq +.el .ds Aq ' +.ss \n[.ss] 0 +.nh +.ad l +.de URL +\fI\\$2\fP <\\$1>\\$3 +.. +.als MTO URL +.if \n[.g] \{\ +. mso www.tmac +. am URL +. ad l +. . +. am MTO +. ad l +. . +. LINKSTYLE blue R < > +.\} +.SH "NAME" +pgpainless\-cli\-verify \- Verify a detached signature +.SH "SYNOPSIS" +.sp +\fBpgpainless\-cli verify\fP [\fB\-\-stacktrace\fP] [\fB\-\-not\-after\fP=\fIDATE\fP] [\fB\-\-not\-before\fP=\fIDATE\fP] \fISIGNATURE\fP +\fICERT\fP... +.SH "DESCRIPTION" +.sp +Verify a detached signature over some data from STDIN. +.SH "OPTIONS" +.sp +\fB\-\-not\-after\fP=\fIDATE\fP +.RS 4 +ISO\-8601 formatted UTC date (e.g. \(aq2020\-11\-23T16:35Z) +.sp +Reject signatures with a creation date not in range. +.sp +Defaults to current system time ("now"). +.sp +Accepts special value "\-" for end of time. +.RE +.sp +\fB\-\-not\-before\fP=\fIDATE\fP +.RS 4 +ISO\-8601 formatted UTC date (e.g. \(aq2020\-11\-23T16:35Z) +.sp +Reject signatures with a creation date not in range. +.sp +Defaults to beginning of time ("\-"). +.RE +.sp +\fB\-\-stacktrace\fP +.RS 4 +Print stacktrace +.RE +.SH "ARGUMENTS" +.sp +\fISIGNATURE\fP +.RS 4 +Detached signature +.RE +.sp +\fICERT\fP... +.RS 4 +Public key certificates for signature verification +.RE \ No newline at end of file diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-version.1 b/pgpainless-cli/packaging/man/pgpainless-cli-version.1 new file mode 100644 index 00000000..c552bbe0 --- /dev/null +++ b/pgpainless-cli/packaging/man/pgpainless-cli-version.1 @@ -0,0 +1,56 @@ +'\" t +.\" Title: pgpainless-cli-version +.\" Author: [see the "AUTHOR(S)" section] +.\" Generator: Asciidoctor 2.0.10 +.\" Manual: PGPainless-CLI Manual +.\" Source: +.\" Language: English +.\" +.TH "PGPAINLESS\-CLI\-VERSION" "1" "" "" "PGPainless\-CLI Manual" +.ie \n(.g .ds Aq \(aq +.el .ds Aq ' +.ss \n[.ss] 0 +.nh +.ad l +.de URL +\fI\\$2\fP <\\$1>\\$3 +.. +.als MTO URL +.if \n[.g] \{\ +. mso www.tmac +. am URL +. ad l +. . +. am MTO +. ad l +. . +. LINKSTYLE blue R < > +.\} +.SH "NAME" +pgpainless\-cli\-version \- Display version information about the tool +.SH "SYNOPSIS" +.sp +\fBpgpainless\-cli version\fP [\fB\-\-stacktrace\fP] [\fB\-\-extended\fP | \fB\-\-backend\fP | \fB\-\-pgpainless\-cli\-spec\fP | \fB\-\-sopv\fP] +.SH "DESCRIPTION" + +.SH "OPTIONS" +.sp +\fB\-\-backend\fP +.RS 4 +Print information about the cryptographic backend +.RE +.sp +\fB\-\-extended\fP +.RS 4 +Print an extended version string +.RE +.sp +\fB\-\-pgpainless\-cli\-spec\fP +.RS 4 +Print the latest revision of the SOP specification targeted by the implementation +.RE +.sp +\fB\-\-sopv\fP, \fB\-\-stacktrace\fP +.RS 4 +Print stacktrace +.RE \ No newline at end of file diff --git a/pgpainless-cli/packaging/man/pgpainless-cli.1 b/pgpainless-cli/packaging/man/pgpainless-cli.1 new file mode 100644 index 00000000..f23468da --- /dev/null +++ b/pgpainless-cli/packaging/man/pgpainless-cli.1 @@ -0,0 +1,233 @@ +'\" t +.\" Title: pgpainless-cli +.\" Author: [see the "AUTHOR(S)" section] +.\" Generator: Asciidoctor 2.0.10 +.\" Manual: PGPainless-CLI Manual +.\" Source: +.\" Language: English +.\" +.TH "PGPAINLESS\-CLI" "1" "" "" "PGPainless\-CLI Manual" +.ie \n(.g .ds Aq \(aq +.el .ds Aq ' +.ss \n[.ss] 0 +.nh +.ad l +.de URL +\fI\\$2\fP <\\$1>\\$3 +.. +.als MTO URL +.if \n[.g] \{\ +. mso www.tmac +. am URL +. ad l +. . +. am MTO +. ad l +. . +. LINKSTYLE blue R < > +.\} +.SH "NAME" +pgpainless\-cli \- Stateless OpenPGP Protocol +.SH "SYNOPSIS" +.sp +\fBpgpainless\-cli\fP [\fB\-\-stacktrace\fP] [COMMAND] +.SH "DESCRIPTION" + +.SH "OPTIONS" +.sp +\fB\-\-stacktrace\fP +.RS 4 +Print stacktrace +.RE +.SH "COMMANDS" +.sp +\fBversion\fP +.RS 4 +Display version information about the tool +.RE +.sp +\fBlist\-profiles\fP +.RS 4 +Emit a list of profiles supported by the identified subcommand +.RE +.sp +\fBgenerate\-key\fP +.RS 4 +Generate a secret key +.RE +.sp +\fBchange\-key\-password\fP +.RS 4 +Update the password of a key +.RE +.sp +\fBrevoke\-key\fP +.RS 4 +Generate revocation certificates +.RE +.sp +\fBextract\-cert\fP +.RS 4 +Extract a public key certificate from a secret key +.RE +.sp +\fBsign\fP +.RS 4 +Create a detached message signature +.RE +.sp +\fBverify\fP +.RS 4 +Verify a detached signature +.RE +.sp +\fBencrypt\fP +.RS 4 +Encrypt a message from standard input +.RE +.sp +\fBdecrypt\fP +.RS 4 +Decrypt a message +.RE +.sp +\fBinline\-detach\fP +.RS 4 +Split signatures from a clearsigned message +.RE +.sp +\fBinline\-sign\fP +.RS 4 +Create an inline\-signed message +.RE +.sp +\fBinline\-verify\fP +.RS 4 +Verify an inline\-signed message +.RE +.sp +\fBarmor\fP +.RS 4 +Add ASCII Armor to standard input +.RE +.sp +\fBdearmor\fP +.RS 4 +Remove ASCII Armor from standard input +.RE +.sp +\fBhelp\fP +.RS 4 +Stateless OpenPGP Protocol +.RE +.sp +\fBgenerate\-completion\fP +.RS 4 +Stateless OpenPGP Protocol +.RE +.SH "EXIT CODES:" +.sp +\fB0\fP +.RS 4 +Successful program execution +.RE +.sp +\fB1\fP +.RS 4 +Generic program error +.RE +.sp +\fB3\fP +.RS 4 +Verification requested but no verifiable signature found +.RE +.sp +\fB13\fP +.RS 4 +Unsupported asymmetric algorithm +.RE +.sp +\fB17\fP +.RS 4 +Certificate is not encryption capable +.RE +.sp +\fB19\fP +.RS 4 +Usage error: Missing argument +.RE +.sp +\fB23\fP +.RS 4 +Incomplete verification instructions +.RE +.sp +\fB29\fP +.RS 4 +Unable to decrypt +.RE +.sp +\fB31\fP +.RS 4 +Password is not human\-readable +.RE +.sp +\fB37\fP +.RS 4 +Unsupported Option +.RE +.sp +\fB41\fP +.RS 4 +Invalid data or data of wrong type encountered +.RE +.sp +\fB53\fP +.RS 4 +Non\-text input received where text was expected +.RE +.sp +\fB59\fP +.RS 4 +Output file already exists +.RE +.sp +\fB61\fP +.RS 4 +Input file does not exist +.RE +.sp +\fB67\fP +.RS 4 +Cannot unlock password protected secret key +.RE +.sp +\fB69\fP +.RS 4 +Unsupported subcommand +.RE +.sp +\fB71\fP +.RS 4 +Unsupported special prefix (e.g. "@ENV/@FD") of indirect parameter +.RE +.sp +\fB73\fP +.RS 4 +Ambiguous input (a filename matching the designator already exists) +.RE +.sp +\fB79\fP +.RS 4 +Key is not signing capable +.RE +.sp +\fB83\fP +.RS 4 +Options were supplied that are incompatible with each other +.RE +.sp +\fB89\fP +.RS 4 +The requested profile is unsupported, or the indicated subcommand does not accept profiles +.RE \ No newline at end of file diff --git a/pgpainless-cli/rewriteManPages.sh b/pgpainless-cli/rewriteManPages.sh new file mode 100755 index 00000000..730af68d --- /dev/null +++ b/pgpainless-cli/rewriteManPages.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash + +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +SOP_DIR=$(realpath $SCRIPT_DIR/../../sop-java) +[ ! -d "$SOP_DIR" ] && echo "sop-java repository MUST be cloned next to pgpainless repo" && exit 1; +SRC_DIR=$SOP_DIR/sop-java-picocli/build/docs/manpage +[ ! -d "$SRC_DIR" ] && echo "No sop manpages found. Please run 'gradle asciidoctor' in the sop-java repo." && exit 1; +DEST_DIR=$SCRIPT_DIR/packaging/man +mkdir -p $DEST_DIR + +for page in $SRC_DIR/* +do + SRC="${page##*/}" + DEST="${SRC/sop/pgpainless-cli}" + sed \ + -e 's/sopv/PLACEHOLDERV/g' \ + -e 's#.\\" Title: sop#.\\" Title: pgpainless-cli#g' \ + -e 's/Manual: Sop Manual/Manual: PGPainless-CLI Manual/g' \ + -e 's/.TH "SOP/.TH "PGPAINLESS\\-CLI/g' \ + -e 's/"Sop Manual"/"PGPainless\\-CLI Manual"/g' \ + -e 's/\\fBsop/\\fBpgpainless\\-cli/g' \ + -e 's/sop/pgpainless\\-cli/g' \ + -e 's/PLACEHOLDERV/sopv/g' \ + $page > $DEST_DIR/$DEST +done + diff --git a/pgpainless-cli/src/main/java/org/pgpainless/cli/PGPainlessCLI.java b/pgpainless-cli/src/main/java/org/pgpainless/cli/PGPainlessCLI.java index b199ec72..7625dd17 100644 --- a/pgpainless-cli/src/main/java/org/pgpainless/cli/PGPainlessCLI.java +++ b/pgpainless-cli/src/main/java/org/pgpainless/cli/PGPainlessCLI.java @@ -7,13 +7,25 @@ package org.pgpainless.cli; import org.pgpainless.sop.SOPImpl; import sop.cli.picocli.SopCLI; +/** + * This class merely binds PGPainless to {@link SopCLI} by injecting a {@link SOPImpl} instance. + * CLI command calls are then simply forwarded to {@link SopCLI#execute(String[])}. + */ public class PGPainlessCLI { static { + // Prevent slf4j initialization logging + // https://github.com/qos-ch/slf4j/issues/422#issuecomment-2277280185 + System.setProperty("slf4j.internal.verbosity", "WARN"); + SopCLI.EXECUTABLE_NAME = "pgpainless-cli"; SopCLI.setSopInstance(new SOPImpl()); } + /** + * Main method of the CLI application. + * @param args arguments + */ public static void main(String[] args) { int result = execute(args); if (result != 0) { @@ -21,6 +33,12 @@ public class PGPainlessCLI { } } + /** + * Execute the given command and return the exit code of the program. + * + * @param args command string array (e.g. ["pgpainless-cli", "generate-key", "Alice"]) + * @return exit code + */ public static int execute(String... args) { return SopCLI.execute(args); } diff --git a/pgpainless-cli/src/main/resources/META-INF/native-image/jni-config.json b/pgpainless-cli/src/main/resources/META-INF/native-image/jni-config.json new file mode 100644 index 00000000..32960f8c --- /dev/null +++ b/pgpainless-cli/src/main/resources/META-INF/native-image/jni-config.json @@ -0,0 +1,2 @@ +[ +] \ No newline at end of file diff --git a/pgpainless-cli/src/main/resources/META-INF/native-image/predefined-classes-config.json b/pgpainless-cli/src/main/resources/META-INF/native-image/predefined-classes-config.json new file mode 100644 index 00000000..84789507 --- /dev/null +++ b/pgpainless-cli/src/main/resources/META-INF/native-image/predefined-classes-config.json @@ -0,0 +1,7 @@ +[ + { + "type":"agent-extracted", + "classes":[ + ] + } +] diff --git a/pgpainless-cli/src/main/resources/META-INF/native-image/proxy-config.json b/pgpainless-cli/src/main/resources/META-INF/native-image/proxy-config.json new file mode 100644 index 00000000..32960f8c --- /dev/null +++ b/pgpainless-cli/src/main/resources/META-INF/native-image/proxy-config.json @@ -0,0 +1,2 @@ +[ +] \ No newline at end of file diff --git a/pgpainless-cli/src/main/resources/META-INF/native-image/reflect-config.json b/pgpainless-cli/src/main/resources/META-INF/native-image/reflect-config.json new file mode 100644 index 00000000..63bdf5f3 --- /dev/null +++ b/pgpainless-cli/src/main/resources/META-INF/native-image/reflect-config.json @@ -0,0 +1,891 @@ +[ +{ + "name":"[Ljava.lang.Object;" +}, +{ + "name":"ch.qos.logback.classic.encoder.PatternLayoutEncoder", + "queryAllPublicMethods":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"ch.qos.logback.classic.joran.SerializedModelConfigurator", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"ch.qos.logback.classic.util.DefaultJoranConfigurator", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"ch.qos.logback.core.ConsoleAppender", + "queryAllPublicMethods":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"setTarget","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"ch.qos.logback.core.OutputStreamAppender", + "methods":[{"name":"setEncoder","parameterTypes":["ch.qos.logback.core.encoder.Encoder"] }] +}, +{ + "name":"ch.qos.logback.core.encoder.Encoder", + "methods":[{"name":"valueOf","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"ch.qos.logback.core.encoder.LayoutWrappingEncoder", + "methods":[{"name":"setParent","parameterTypes":["ch.qos.logback.core.spi.ContextAware"] }] +}, +{ + "name":"ch.qos.logback.core.pattern.PatternLayoutEncoderBase", + "methods":[{"name":"setPattern","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"ch.qos.logback.core.spi.ContextAware", + "methods":[{"name":"valueOf","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"com.sun.org.apache.xerces.internal.jaxp.SAXParserFactoryImpl", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"groovy.lang.Closure" +}, +{ + "name":"java.io.FilePermission" +}, +{ + "name":"java.lang.Enum" +}, +{ + "name":"java.lang.Object", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true +}, +{ + "name":"java.lang.RuntimePermission" +}, +{ + "name":"java.lang.System", + "methods":[{"name":"console","parameterTypes":[] }] +}, +{ + "name":"java.lang.invoke.MethodHandle" +}, +{ + "name":"java.net.NetPermission" +}, +{ + "name":"java.net.SocketPermission" +}, +{ + "name":"java.net.URLPermission", + "methods":[{"name":"","parameterTypes":["java.lang.String","java.lang.String"] }] +}, +{ + "name":"java.nio.channels.SelectionKey", + "fields":[{"name":"attachment"}] +}, +{ + "name":"java.nio.file.Path" +}, +{ + "name":"java.nio.file.Paths", + "methods":[{"name":"get","parameterTypes":["java.lang.String","java.lang.String[]"] }] +}, +{ + "name":"java.security.AllPermission" +}, +{ + "name":"java.security.MessageDigestSpi" +}, +{ + "name":"java.security.SecureRandomParameters" +}, +{ + "name":"java.security.SecurityPermission" +}, +{ + "name":"java.security.cert.PKIXRevocationChecker" +}, +{ + "name":"java.sql.Connection" +}, +{ + "name":"java.sql.Driver" +}, +{ + "name":"java.sql.DriverManager", + "methods":[{"name":"getConnection","parameterTypes":["java.lang.String"] }, {"name":"getDriver","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"java.sql.Time", + "methods":[{"name":"","parameterTypes":["long"] }] +}, +{ + "name":"java.sql.Timestamp", + "methods":[{"name":"valueOf","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"java.time.Duration", + "methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }] +}, +{ + "name":"java.time.Instant", + "methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }] +}, +{ + "name":"java.time.LocalDate", + "methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }] +}, +{ + "name":"java.time.LocalDateTime", + "methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }] +}, +{ + "name":"java.time.LocalTime", + "methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }] +}, +{ + "name":"java.time.MonthDay", + "methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }] +}, +{ + "name":"java.time.OffsetDateTime", + "methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }] +}, +{ + "name":"java.time.OffsetTime", + "methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }] +}, +{ + "name":"java.time.Period", + "methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }] +}, +{ + "name":"java.time.Year", + "methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }] +}, +{ + "name":"java.time.YearMonth", + "methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }] +}, +{ + "name":"java.time.ZoneId", + "methods":[{"name":"of","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"java.time.ZoneOffset", + "methods":[{"name":"of","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"java.time.ZonedDateTime", + "methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }] +}, +{ + "name":"java.util.HashSet" +}, +{ + "name":"java.util.LinkedHashSet" +}, +{ + "name":"java.util.PropertyPermission" +}, +{ + "name":"java.util.concurrent.ArrayBlockingQueue" +}, +{ + "name":"java.util.concurrent.atomic.AtomicReference", + "fields":[{"name":"value"}] +}, +{ + "name":"java.util.concurrent.locks.AbstractOwnableSynchronizer" +}, +{ + "name":"java.util.concurrent.locks.AbstractQueuedSynchronizer" +}, +{ + "name":"java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject" +}, +{ + "name":"java.util.concurrent.locks.ReentrantLock" +}, +{ + "name":"java.util.concurrent.locks.ReentrantLock$NonfairSync" +}, +{ + "name":"java.util.concurrent.locks.ReentrantLock$Sync" +}, +{ + "name":"javax.smartcardio.CardPermission" +}, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.COMPOSITE$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.CONTEXT$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.CompositeSignatures$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.DH$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.DSA$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.DSTU4145$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.Dilithium$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.EC$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.ECGOST$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.EXTERNAL$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.EdEC$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.ElGamal$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.Falcon$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.GM$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.GOST$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.IES$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.LMS$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.MLDSA$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.MLKEM$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.NTRU$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.NoSig$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.RSA$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.SLHDSA$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.SPHINCSPlus$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.X509$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.edec.KeyPairGeneratorSpi$EdDSA", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.edec.KeyPairGeneratorSpi$XDH", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.rsa.KeyPairGeneratorSpi", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.Blake2b$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.Blake2s$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.Blake3$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.DSTU7564$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.GOST3411$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.Haraka$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.Keccak$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.MD2$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.MD4$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.MD5$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.RIPEMD128$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.RIPEMD160$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.RIPEMD256$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.RIPEMD320$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.SHA1$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.SHA224$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.SHA256$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.SHA3$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.SHA384$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.SHA512$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.SM3$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.Skein$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.Tiger$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.Whirlpool$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.drbg.DRBG$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.keystore.BC$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.keystore.BCFKS$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.keystore.PKCS12$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.AES$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.ARC4$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.ARIA$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.Blowfish$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.CAST5$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.CAST6$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.Camellia$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.ChaCha$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.DES$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.DESede$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.DSTU7624$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.GOST28147$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.GOST3412_2015$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.Grain128$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.Grainv1$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.HC128$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.HC256$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.IDEA$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.Noekeon$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.OpenSSLPBKDF$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.PBEPBKDF1$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.PBEPBKDF2$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.PBEPKCS12$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.Poly1305$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.RC2$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.RC5$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.RC6$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.Rijndael$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.SCRYPT$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.SEED$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.SM4$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.Salsa20$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.Serpent$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.Shacal2$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.SipHash$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.SipHash128$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.Skipjack$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.TEA$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.TLSKDF$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.Threefish$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.Twofish$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.VMPC$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.VMPCKSA3$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.XSalsa20$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.XTEA$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.Zuc$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.pgpainless.cli.ExitCodeTest", + "allDeclaredFields":true, + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"successfulExecutionDoesNotTerminateJVM","parameterTypes":[] }, {"name":"testCommandWithUnknownOption_37","parameterTypes":[] }, {"name":"testUnknownCommand_69","parameterTypes":[] }] +}, +{ + "name":"org.pgpainless.cli.TestUtils", + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.pgpainless.cli.commands.ArmorCmdTest", + "allDeclaredFields":true, + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"armorAlreadyArmoredDataIsIdempotent","parameterTypes":[] }, {"name":"armorMessage","parameterTypes":[] }, {"name":"armorPublicKey","parameterTypes":[] }, {"name":"armorSecretKey","parameterTypes":[] }] +}, +{ + "name":"org.pgpainless.cli.commands.CLITest", + "allDeclaredFields":true, + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"cleanup","parameterTypes":[] }, {"name":"setup","parameterTypes":[] }] +}, +{ + "name":"org.pgpainless.cli.commands.DearmorCmdTest", + "allDeclaredFields":true, + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"dearmorBrokenArmoredKeyFails","parameterTypes":[] }, {"name":"dearmorCertificate","parameterTypes":[] }, {"name":"dearmorGarbageEmitsEmpty","parameterTypes":[] }, {"name":"dearmorMessage","parameterTypes":[] }, {"name":"dearmorSecretKey","parameterTypes":[] }] +}, +{ + "name":"org.pgpainless.cli.commands.ExtractCertCmdTest", + "allDeclaredFields":true, + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"extractCertFromGarbageFails","parameterTypes":[] }, {"name":"testExtractCert","parameterTypes":[] }, {"name":"testExtractCertFromCertFails","parameterTypes":[] }, {"name":"testExtractCertUnarmored","parameterTypes":[] }] +}, +{ + "name":"org.pgpainless.cli.commands.GenerateKeyCmdTest", + "allDeclaredFields":true, + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"testGenerateBinaryKey","parameterTypes":[] }, {"name":"testGenerateKey","parameterTypes":[] }, {"name":"testGenerateKeyWithMultipleUserIds","parameterTypes":[] }, {"name":"testGeneratePasswordProtectedKey_missingPasswordFile","parameterTypes":[] }, {"name":"testPasswordProtectedKey","parameterTypes":[] }] +}, +{ + "name":"org.pgpainless.cli.commands.InlineDetachCmdTest", + "allDeclaredFields":true, + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"detachInbandSignatureAndMessage","parameterTypes":[] }, {"name":"detachInbandSignatureAndMessageNoArmor","parameterTypes":[] }, {"name":"detachMissingSignaturesFromCleartextSignedMessageFails","parameterTypes":[] }, {"name":"detachNonOpenPgpDataFails","parameterTypes":[] }, {"name":"existingSignatureOutCausesException","parameterTypes":[] }] +}, +{ + "name":"org.pgpainless.cli.commands.ListProfilesCmdTest", + "allDeclaredFields":true, + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"listProfileOfGenerateKey","parameterTypes":[] }, {"name":"listProfilesOfEncrypt","parameterTypes":[] }, {"name":"listProfilesWithoutCommand","parameterTypes":[] }] +}, +{ + "name":"org.pgpainless.cli.commands.RoundTripEncryptDecryptCmdTest", + "allDeclaredFields":true, + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"decryptGarbageFails","parameterTypes":[] }, {"name":"decryptMalformedMessageYieldsBadData","parameterTypes":[] }, {"name":"decryptMessageWithSessionKey","parameterTypes":[] }, {"name":"decryptMessageWithWrongKeyFails","parameterTypes":[] }, {"name":"decryptWithPasswordWithPendingWhitespaceWorks","parameterTypes":[] }, {"name":"decryptWithWhitespacePasswordWorks","parameterTypes":[] }, {"name":"decrypt_verifyWithGarbageCertFails","parameterTypes":[] }, {"name":"decrypt_withGarbageKeyFails","parameterTypes":[] }, {"name":"encryptAndDecryptAMessage","parameterTypes":[] }, {"name":"encryptAndDecryptMessageWithPassphrase","parameterTypes":[] }, {"name":"encryptWithGarbageCertFails","parameterTypes":[] }, {"name":"encryptWithPasswordADecryptWithPasswordBFails","parameterTypes":[] }, {"name":"encryptWithProtectedKey_wrongPassphraseFails","parameterTypes":[] }, {"name":"encryptWithTrailingWhitespaceDecryptWithoutWorks","parameterTypes":[] }, {"name":"encrypt_signWithGarbageKeyFails","parameterTypes":[] }, {"name":"testDecryptVerifyOut_withoutVerifyWithFails","parameterTypes":[] }, {"name":"testDecryptWithSessionKeyVerifyWithYieldsExpectedVerifications","parameterTypes":[] }, {"name":"testDecryptWithoutDecryptionOptionFails","parameterTypes":[] }, {"name":"testEncryptDecryptRoundTripWithPasswordProtectedKey","parameterTypes":[] }, {"name":"testEncryptDecryptWithFreshRSAKey","parameterTypes":[] }, {"name":"testEncryptWithIncapableCert","parameterTypes":[] }, {"name":"testEncrypt_SignWithCertFails","parameterTypes":[] }, {"name":"testMissingArgumentsIfNoArgsSupplied","parameterTypes":[] }, {"name":"testSessionKeyOutWritesSessionKeyOut","parameterTypes":[] }, {"name":"testSignWithIncapableKey","parameterTypes":[] }, {"name":"testVerificationsOutAlreadyExistFails","parameterTypes":[] }] +}, +{ + "name":"org.pgpainless.cli.commands.RoundTripInlineSignInlineVerifyCmdTest", + "allDeclaredFields":true, + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"cannotVerifyEncryptedMessage","parameterTypes":[] }, {"name":"cannotVerifyMalformedMessage","parameterTypes":[] }, {"name":"createAndVerifyCleartextSignedMessage","parameterTypes":[] }, {"name":"createAndVerifyMultiKeyBinarySignedMessage","parameterTypes":[] }, {"name":"createAndVerifyTextSignedMessage","parameterTypes":[] }, {"name":"createCleartextSignedMessage","parameterTypes":[] }, {"name":"createMalformedMessage","parameterTypes":[] }, {"name":"createSignedMessageWithKeyAAndVerifyWithKeyBFails","parameterTypes":[] }, {"name":"createTextSignedMessageInlineDetachAndDetachedVerify","parameterTypes":[] }, {"name":"signWithProtectedKeyWithWrongPassphraseFails","parameterTypes":[] }, {"name":"testInlineSignWithMissingSecretKeysFails","parameterTypes":[] }, {"name":"testUnlockKeyWithOneOfMultiplePasswords","parameterTypes":[] }, {"name":"verifyPrependedSignedMessage","parameterTypes":[] }] +}, +{ + "name":"org.pgpainless.cli.commands.RoundTripInlineSignVerifyCmdTest", + "allDeclaredFields":true, + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"encryptAndDecryptAMessage","parameterTypes":[] }] +}, +{ + "name":"org.pgpainless.cli.commands.RoundTripSignVerifyCmdTest", + "allDeclaredFields":true, + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"createArmoredSignature","parameterTypes":[] }, {"name":"createUnarmoredSignature","parameterTypes":[] }, {"name":"signWithProtectedKey","parameterTypes":[] }, {"name":"signWithProtectedKey_missingPassphraseFails","parameterTypes":[] }, {"name":"signWithProtectedKey_wrongPassphraseFails","parameterTypes":[] }, {"name":"testNotAfter","parameterTypes":[] }, {"name":"testNotBefore","parameterTypes":[] }, {"name":"testSignWithIncapableKey","parameterTypes":[] }, {"name":"testSignatureCreationAndVerification","parameterTypes":[] }, {"name":"unarmorArmoredSigAndVerify","parameterTypes":[] }] +}, +{ + "name":"org.pgpainless.cli.commands.VersionCmdTest", + "allDeclaredFields":true, + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"testExtendedVersion","parameterTypes":[] }, {"name":"testGetBackendVersion","parameterTypes":[] }, {"name":"testSopSpecVersion","parameterTypes":[] }, {"name":"testVersion","parameterTypes":[] }] +}, +{ + "name":"org.pgpainless.cli.misc.SignUsingPublicKeyBehaviorTest", + "allDeclaredFields":true, + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"testSignatureCreationAndVerification","parameterTypes":[] }] +}, +{ + "name":"picocli.AutoComplete$GenerateCompletion", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"picocli.CommandLine$AutoHelpMixin", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true +}, +{ + "name":"picocli.CommandLine$HelpCommand", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true +}, +{ + "name":"sop.cli.picocli.SopCLI", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sop.cli.picocli.SopCLI$InitLocale", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true +}, +{ + "name":"sop.cli.picocli.commands.AbstractSopCmd", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true +}, +{ + "name":"sop.cli.picocli.commands.ArmorCmd", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sop.cli.picocli.commands.ChangeKeyPasswordCmd", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true +}, +{ + "name":"sop.cli.picocli.commands.DearmorCmd", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sop.cli.picocli.commands.DecryptCmd", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sop.cli.picocli.commands.EncryptCmd", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sop.cli.picocli.commands.ExtractCertCmd", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sop.cli.picocli.commands.GenerateKeyCmd", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sop.cli.picocli.commands.InlineDetachCmd", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sop.cli.picocli.commands.InlineSignCmd", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sop.cli.picocli.commands.InlineVerifyCmd", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sop.cli.picocli.commands.ListProfilesCmd", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sop.cli.picocli.commands.RevokeKeyCmd", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true +}, +{ + "name":"sop.cli.picocli.commands.SignCmd", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sop.cli.picocli.commands.VerifyCmd", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sop.cli.picocli.commands.VersionCmd", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sop.cli.picocli.commands.VersionCmd$Exclusive", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.provider.NativePRNG", + "methods":[{"name":"","parameterTypes":[] }, {"name":"","parameterTypes":["java.security.SecureRandomParameters"] }] +}, +{ + "name":"sun.security.provider.SHA", + "methods":[{"name":"","parameterTypes":[] }] +} +] \ No newline at end of file diff --git a/pgpainless-cli/src/main/resources/META-INF/native-image/resource-config.json b/pgpainless-cli/src/main/resources/META-INF/native-image/resource-config.json new file mode 100644 index 00000000..3c66a520 --- /dev/null +++ b/pgpainless-cli/src/main/resources/META-INF/native-image/resource-config.json @@ -0,0 +1,93 @@ +{ + "resources":{ + "includes":[{ + "pattern":"\\QMETA-INF/services/ch.qos.logback.classic.spi.Configurator\\E" + }, { + "pattern":"\\QMETA-INF/services/java.lang.System$LoggerFinder\\E" + }, { + "pattern":"\\QMETA-INF/services/java.nio.channels.spi.SelectorProvider\\E" + }, { + "pattern":"\\QMETA-INF/services/java.time.zone.ZoneRulesProvider\\E" + }, { + "pattern":"\\QMETA-INF/services/java.util.spi.ResourceBundleControlProvider\\E" + }, { + "pattern":"\\QMETA-INF/services/javax.xml.parsers.SAXParserFactory\\E" + }, { + "pattern":"\\QMETA-INF/services/org.junit.platform.engine.TestEngine\\E" + }, { + "pattern":"\\QMETA-INF/services/org.junit.platform.launcher.LauncherDiscoveryListener\\E" + }, { + "pattern":"\\QMETA-INF/services/org.junit.platform.launcher.LauncherSessionListener\\E" + }, { + "pattern":"\\QMETA-INF/services/org.junit.platform.launcher.PostDiscoveryFilter\\E" + }, { + "pattern":"\\QMETA-INF/services/org.junit.platform.launcher.TestExecutionListener\\E" + }, { + "pattern":"\\QMETA-INF/services/org.slf4j.spi.SLF4JServiceProvider\\E" + }, { + "pattern":"\\Qjunit-platform.properties\\E" + }, { + "pattern":"\\Qlogback-test.scmo\\E" + }, { + "pattern":"\\Qlogback-test.xml\\E" + }, { + "pattern":"\\Qlogback.scmo\\E" + }, { + "pattern":"\\Qorg/slf4j/impl/StaticLoggerBinder.class\\E" + }, { + "pattern":"\\Qpgpainless-sop.properties\\E" + }, { + "pattern":"\\Qsop-java-version.properties\\E" + }, { + "pattern":"java.base:\\Qsun/text/resources/LineBreakIteratorData\\E" + }]}, + "bundles":[{ + "name":"msg_armor", + "locales":["de", "und"] + }, { + "name":"msg_change-key-password", + "locales":["de", "und"] + }, { + "name":"msg_dearmor", + "locales":["de", "und"] + }, { + "name":"msg_decrypt", + "locales":["de", "und"] + }, { + "name":"msg_detached-sign", + "locales":["de", "und"] + }, { + "name":"msg_detached-verify", + "locales":["de", "und"] + }, { + "name":"msg_encrypt", + "locales":["de", "und"] + }, { + "name":"msg_extract-cert", + "locales":["de", "und"] + }, { + "name":"msg_generate-key", + "locales":["de", "und"] + }, { + "name":"msg_inline-detach", + "locales":["de", "und"] + }, { + "name":"msg_inline-sign", + "locales":["de", "und"] + }, { + "name":"msg_inline-verify", + "locales":["de", "und"] + }, { + "name":"msg_list-profiles", + "locales":["de", "und"] + }, { + "name":"msg_revoke-key", + "locales":["de", "und"] + }, { + "name":"msg_sop", + "locales":["de", "und"] + }, { + "name":"msg_version", + "locales":["de", "und"] + }] +} \ No newline at end of file diff --git a/pgpainless-cli/src/main/resources/META-INF/native-image/serialization-config.json b/pgpainless-cli/src/main/resources/META-INF/native-image/serialization-config.json new file mode 100644 index 00000000..3f06d9a8 --- /dev/null +++ b/pgpainless-cli/src/main/resources/META-INF/native-image/serialization-config.json @@ -0,0 +1,41 @@ +{ + "types":[ + { + "name":"java.lang.Enum" + }, + { + "name":"java.lang.Object[]" + }, + { + "name":"java.util.HashSet" + }, + { + "name":"java.util.LinkedHashSet" + }, + { + "name":"java.util.concurrent.ArrayBlockingQueue" + }, + { + "name":"java.util.concurrent.locks.AbstractOwnableSynchronizer" + }, + { + "name":"java.util.concurrent.locks.AbstractQueuedSynchronizer" + }, + { + "name":"java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject" + }, + { + "name":"java.util.concurrent.locks.ReentrantLock" + }, + { + "name":"java.util.concurrent.locks.ReentrantLock$NonfairSync" + }, + { + "name":"java.util.concurrent.locks.ReentrantLock$Sync" + } + ], + "lambdaCapturingTypes":[ + ], + "proxies":[ + ] +} \ No newline at end of file diff --git a/pgpainless-cli/src/main/resources/logback.xml b/pgpainless-cli/src/main/resources/logback.xml index 13cccf62..8451d6a4 100644 --- a/pgpainless-cli/src/main/resources/logback.xml +++ b/pgpainless-cli/src/main/resources/logback.xml @@ -4,15 +4,6 @@ SPDX-FileCopyrightText: 2021 Paul Schaub SPDX-License-Identifier: Apache-2.0 --> - - - System.err - - %blue(%-5level) %green(%logger{35}) - %msg %n - - - - - - + + \ No newline at end of file diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/ExitCodeTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/ExitCodeTest.java index aa3d7d5b..b1ca143a 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/ExitCodeTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/ExitCodeTest.java @@ -4,27 +4,35 @@ package org.pgpainless.cli; -import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; -import com.ginsberg.junit.exit.FailOnSystemExit; import org.junit.jupiter.api.Test; +import org.pgpainless.cli.commands.CLITest; +import org.slf4j.LoggerFactory; +import sop.exception.SOPGPException; -public class ExitCodeTest { +import java.io.IOException; - @Test - @ExpectSystemExitWithStatus(69) - public void testUnknownCommand_69() { - PGPainlessCLI.main(new String[] {"generate-kex"}); +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class ExitCodeTest extends CLITest { + + public ExitCodeTest() { + super(LoggerFactory.getLogger(ExitCodeTest.class)); } @Test - @ExpectSystemExitWithStatus(37) - public void testCommandWithUnknownOption_37() { - PGPainlessCLI.main(new String[] {"generate-key", "-k", "\"k is unknown\""}); + public void testUnknownCommand_69() throws IOException { + assertEquals(SOPGPException.UnsupportedSubcommand.EXIT_CODE, + executeCommand("unsupported-subcommand")); } @Test - @FailOnSystemExit - public void successfulExecutionDoesNotTerminateJVM() { - PGPainlessCLI.main(new String[] {"version"}); + public void testCommandWithUnknownOption_37() throws IOException { + assertEquals(SOPGPException.UnsupportedOption.EXIT_CODE, + executeCommand("generate-key", "-k", "\"k is unknown\"")); + } + + @Test + public void successfulExecutionDoesNotTerminateJVM() throws IOException { + assertSuccess(executeCommand("version")); } } diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/TestUtils.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/TestUtils.java index 16e1937c..8fca7381 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/TestUtils.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/TestUtils.java @@ -11,6 +11,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; @@ -40,6 +41,17 @@ public class TestUtils { return dir; } + public static File writeTempFile(File tempDir, byte[] value) throws IOException { + File tempFile = new File(tempDir, randomString(10)); + tempFile.createNewFile(); + tempFile.deleteOnExit(); + FileOutputStream fileOutputStream = new FileOutputStream(tempFile); + fileOutputStream.write(value); + fileOutputStream.flush(); + fileOutputStream.close(); + return tempFile; + } + private static String randomString(int length) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < length; i++) { diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ArmorCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ArmorCmdTest.java new file mode 100644 index 00000000..ddb3c6dd --- /dev/null +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ArmorCmdTest.java @@ -0,0 +1,100 @@ +// SPDX-FileCopyrightText: 2021 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.cli.commands; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.slf4j.LoggerFactory; + +public class ArmorCmdTest extends CLITest { + + public ArmorCmdTest() { + super(LoggerFactory.getLogger(ArmorCmdTest.class)); + } + + private static final String key = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 62E9 DDA4 F20F 8341 D2BC 4B4C 8B07 5177 01F9 534C\n" + + "Comment: alice@pgpainless.org\n" + + "\n" + + "lFgEY2vOkhYJKwYBBAHaRw8BAQdAqGOtLd1tKnuwaYYcdr2/7C0cPiCCggRMKG+W\n" + + "t32QQdEAAP9VaBzjk/AaAqyykZnQHmS1HByEvRLv5/4yJMSr22451BFjtBRhbGlj\n" + + "ZUBwZ3BhaW5sZXNzLm9yZ4iOBBMWCgBBBQJja86SCRCLB1F3AflTTBYhBGLp3aTy\n" + + "D4NB0rxLTIsHUXcB+VNMAp4BApsBBRYCAwEABAsJCAcFFQoJCAsCmQEAACZhAP4s\n" + + "8hn/RBDvyLvGROOd15EYATnWlgyi+b5WXP6cELalJwD1FZy3RROhfNtZWcJPS43f\n" + + "G03pYNyb0NXoitIMAaXEB5xdBGNrzpISCisGAQQBl1UBBQEBB0CqCcYethOynfni\n" + + "8uRO+r/cZWp9hCLy8pRIExKqzcyEFAMBCAcAAP9sRRLoZkLpDaTNNrtIBovXu2AN\n" + + "hL8keUMWtVcuEHnkQA6iiHUEGBYKAB0FAmNrzpICngECmwwFFgIDAQAECwkIBwUV\n" + + "CgkICwAKCRCLB1F3AflTTBVpAP491etrjqCMWx2bBaw3K1vP0Mix6U0vF3J4kP9U\n" + + "eZm6owEA4kX9VAGESvLgIc7CEiswmxdWjxnLQyCRtWXfjgFmYQucWARja86SFgkr\n" + + "BgEEAdpHDwEBB0DBslhDpWC6CV3xJUSo071NSO5Cf4fgOwOj+QHs8mpFbwABAPkQ\n" + + "ioSydYiMi04LyfPohyrhhcdJDHallQg+jYHHUb2pEJCI1QQYFgoAfQUCY2vOkgKe\n" + + "AQKbAgUWAgMBAAQLCQgHBRUKCQgLXyAEGRYKAAYFAmNrzpIACgkQiHlkvEXh+f1e\n" + + "ywEA9A2GLU9LxCJxZf2X4qcZY//YJDChIZHPnY0Vaek1DsMBAN1YILrH2rxQeCXj\n" + + "m4bUKfJIRrGt6ZJscwORgNI1dFQFAAoJEIsHUXcB+VNMK3gA/3vvPm57JsHA860w\n" + + "lB4D1II71oFNL8TFnJqTAvpSKe1AAP49S4mKB4PE0ElcDo7n+nEYt6ba8IMRDlMo\n" + + "rsH85mUgCw==\n" + + "=EMKf\n" + + "-----END PGP PRIVATE KEY BLOCK-----\n"; + + @Test + public void armorSecretKey() throws IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(key); + byte[] binary = secretKeys.getEncoded(); + + pipeBytesToStdin(binary); + ByteArrayOutputStream armorOut = pipeStdoutToStream(); + assertSuccess(executeCommand("armor")); + + PGPSecretKeyRing armored = PGPainless.readKeyRing().secretKeyRing(armorOut.toString()); + assertArrayEquals(secretKeys.getEncoded(), armored.getEncoded()); + } + + @Test + public void armorPublicKey() throws IOException { + PGPSecretKeyRing secretKey = PGPainless.readKeyRing().secretKeyRing(key); + PGPPublicKeyRing publicKey = PGPainless.extractCertificate(secretKey); + byte[] bytes = publicKey.getEncoded(); + + pipeBytesToStdin(bytes); + ByteArrayOutputStream armorOut = pipeStdoutToStream(); + assertSuccess(executeCommand("armor")); + + PGPPublicKeyRing armored = PGPainless.readKeyRing().publicKeyRing(armorOut.toString()); + assertArrayEquals(publicKey.getEncoded(), armored.getEncoded()); + } + + @Test + public void armorMessage() throws IOException { + String message = "Hello, World!\n"; + + pipeStringToStdin(message); + ByteArrayOutputStream armorOut = pipeStdoutToStream(); + assertSuccess(executeCommand("armor")); + + String armored = armorOut.toString(); + assertTrue(armored.startsWith("-----BEGIN PGP MESSAGE-----\n")); + assertTrue(armored.contains("SGVsbG8sIFdvcmxkIQo=")); + } + + @Test + public void armorAlreadyArmoredDataIsIdempotent() throws IOException { + pipeStringToStdin(key); + ByteArrayOutputStream armorOut = pipeStdoutToStream(); + assertSuccess(executeCommand("armor")); + + String armored = armorOut.toString(); + assertEquals(key, armored); + } +} diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ArmorTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ArmorTest.java deleted file mode 100644 index 409d4040..00000000 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ArmorTest.java +++ /dev/null @@ -1,91 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.cli.commands; - -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.PrintStream; -import java.nio.charset.StandardCharsets; -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; - -import com.ginsberg.junit.exit.FailOnSystemExit; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.pgpainless.PGPainless; -import org.pgpainless.cli.PGPainlessCLI; - -public class ArmorTest { - - private static PrintStream originalSout; - - @BeforeEach - public void saveSout() { - originalSout = System.out; - } - - @AfterEach - public void restoreSout() { - System.setOut(originalSout); - } - - @Test - @FailOnSystemExit - public void armorSecretKey() throws IOException, PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - PGPSecretKeyRing secretKey = PGPainless.generateKeyRing() - .modernKeyRing("alice@pgpainless.org", null); - byte[] bytes = secretKey.getEncoded(); - - System.setIn(new ByteArrayInputStream(bytes)); - ByteArrayOutputStream armorOut = new ByteArrayOutputStream(); - System.setOut(new PrintStream(armorOut)); - PGPainlessCLI.execute("armor"); - - PGPSecretKeyRing armored = PGPainless.readKeyRing().secretKeyRing(armorOut.toString()); - assertArrayEquals(secretKey.getEncoded(), armored.getEncoded()); - } - - @Test - @FailOnSystemExit - public void armorPublicKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { - PGPSecretKeyRing secretKey = PGPainless.generateKeyRing() - .modernKeyRing("alice@pgpainless.org", null); - PGPPublicKeyRing publicKey = PGPainless.extractCertificate(secretKey); - byte[] bytes = publicKey.getEncoded(); - - System.setIn(new ByteArrayInputStream(bytes)); - ByteArrayOutputStream armorOut = new ByteArrayOutputStream(); - System.setOut(new PrintStream(armorOut)); - PGPainlessCLI.execute("armor"); - - PGPPublicKeyRing armored = PGPainless.readKeyRing().publicKeyRing(armorOut.toString()); - assertArrayEquals(publicKey.getEncoded(), armored.getEncoded()); - } - - @Test - @FailOnSystemExit - public void armorMessage() { - String message = "Hello, World!\n"; - - System.setIn(new ByteArrayInputStream(message.getBytes(StandardCharsets.UTF_8))); - ByteArrayOutputStream armorOut = new ByteArrayOutputStream(); - System.setOut(new PrintStream(armorOut)); - PGPainlessCLI.execute("armor"); - - String armored = armorOut.toString(); - - assertTrue(armored.startsWith("-----BEGIN PGP MESSAGE-----\n")); - assertTrue(armored.contains("SGVsbG8sIFdvcmxkIQo=")); - } - -} diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/CLITest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/CLITest.java new file mode 100644 index 00000000..9e2fd895 --- /dev/null +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/CLITest.java @@ -0,0 +1,167 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.cli.commands; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import javax.annotation.Nonnull; + +import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.opentest4j.TestAbortedException; +import org.pgpainless.cli.TestUtils; +import org.pgpainless.sop.SOPImpl; +import org.slf4j.Logger; +import sop.cli.picocli.SopCLI; + +public abstract class CLITest { + + protected File testDirectory; + protected InputStream stdin; + protected PrintStream stdout; + + protected final Logger LOGGER; + + + public CLITest(@Nonnull Logger logger) { + LOGGER = logger; + SopCLI.setSopInstance(new SOPImpl()); + } + + @BeforeEach + public void setup() throws IOException { + testDirectory = TestUtils.createTempDirectory(); + testDirectory.deleteOnExit(); + LOGGER.debug(testDirectory.getAbsolutePath()); + stdin = System.in; + stdout = System.out; + } + + @AfterEach + public void cleanup() throws IOException { + resetStreams(); + } + + public File nonExistentFile(String name) { + File file = new File(testDirectory, name); + if (file.exists()) { + throw new TestAbortedException("File " + file.getAbsolutePath() + " already exists."); + } + return file; + } + + public File pipeStdoutToFile(String name) throws IOException { + File file = new File(testDirectory, name); + file.deleteOnExit(); + if (!file.createNewFile()) { + throw new TestAbortedException("Cannot create new file " + file.getAbsolutePath()); + } + System.setOut(new PrintStream(Files.newOutputStream(file.toPath()))); + return file; + } + + public ByteArrayOutputStream pipeStdoutToStream() { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + pipeStdoutToStream(out); + return out; + } + + public void pipeStdoutToStream(OutputStream stream) { + System.setOut(new PrintStream(stream)); + } + + public void pipeFileToStdin(File file) throws IOException { + System.setIn(Files.newInputStream(file.toPath())); + } + + public void pipeBytesToStdin(byte[] bytes) { + System.setIn(new ByteArrayInputStream(bytes)); + } + + public void pipeStringToStdin(String string) { + System.setIn(new ByteArrayInputStream(string.getBytes(StandardCharsets.UTF_8))); + } + + public void resetStdout() { + if (System.out != stdout) { + System.out.flush(); + System.out.close(); + } + System.setOut(stdout); + } + + public void resetStdin() throws IOException { + if (System.in != stdin) { + System.in.close(); + } + System.setIn(stdin); + } + + public void resetStreams() throws IOException { + resetStdout(); + resetStdin(); + } + + public File writeFile(String name, String data) throws IOException { + return writeFile(name, data.getBytes(StandardCharsets.UTF_8)); + } + + public File writeFile(String name, byte[] bytes) throws IOException { + return writeFile(name, new ByteArrayInputStream(bytes)); + } + + public File writeFile(String name, InputStream data) throws IOException { + File file = new File(testDirectory, name); + if (!file.createNewFile()) { + throw new TestAbortedException("Cannot create new file " + file.getAbsolutePath()); + } + file.deleteOnExit(); + try (FileOutputStream fileOut = new FileOutputStream(file)) { + Streams.pipeAll(data, fileOut); + fileOut.flush(); + } + return file; + } + + public byte[] readBytesFromFile(File file) { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + try (FileInputStream fileIn = new FileInputStream(file)) { + Streams.pipeAll(fileIn, buffer); + } catch (FileNotFoundException e) { + throw new TestAbortedException("File " + file.getAbsolutePath() + " does not exist!", e); + } catch (IOException e) { + throw new TestAbortedException("Cannot read from file " + file.getAbsolutePath(), e); + } + return buffer.toByteArray(); + } + + public String readStringFromFile(File file) { + return new String(readBytesFromFile(file), StandardCharsets.UTF_8); + } + + public int executeCommand(String... command) throws IOException { + int exitCode = SopCLI.execute(command); + resetStreams(); + return exitCode; + } + + public void assertSuccess(int exitCode) { + assertEquals(0, exitCode, + "Expected successful program execution"); + } +} diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/DearmorCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/DearmorCmdTest.java new file mode 100644 index 00000000..7ebb7308 --- /dev/null +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/DearmorCmdTest.java @@ -0,0 +1,113 @@ +// SPDX-FileCopyrightText: 2021 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.cli.commands; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.slf4j.LoggerFactory; +import sop.exception.SOPGPException; + +public class DearmorCmdTest extends CLITest { + + public DearmorCmdTest() { + super(LoggerFactory.getLogger(DearmorCmdTest.class)); + } + + private static final String key = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 62E9 DDA4 F20F 8341 D2BC 4B4C 8B07 5177 01F9 534C\n" + + "Comment: alice@pgpainless.org\n" + + "\n" + + "lFgEY2vOkhYJKwYBBAHaRw8BAQdAqGOtLd1tKnuwaYYcdr2/7C0cPiCCggRMKG+W\n" + + "t32QQdEAAP9VaBzjk/AaAqyykZnQHmS1HByEvRLv5/4yJMSr22451BFjtBRhbGlj\n" + + "ZUBwZ3BhaW5sZXNzLm9yZ4iOBBMWCgBBBQJja86SCRCLB1F3AflTTBYhBGLp3aTy\n" + + "D4NB0rxLTIsHUXcB+VNMAp4BApsBBRYCAwEABAsJCAcFFQoJCAsCmQEAACZhAP4s\n" + + "8hn/RBDvyLvGROOd15EYATnWlgyi+b5WXP6cELalJwD1FZy3RROhfNtZWcJPS43f\n" + + "G03pYNyb0NXoitIMAaXEB5xdBGNrzpISCisGAQQBl1UBBQEBB0CqCcYethOynfni\n" + + "8uRO+r/cZWp9hCLy8pRIExKqzcyEFAMBCAcAAP9sRRLoZkLpDaTNNrtIBovXu2AN\n" + + "hL8keUMWtVcuEHnkQA6iiHUEGBYKAB0FAmNrzpICngECmwwFFgIDAQAECwkIBwUV\n" + + "CgkICwAKCRCLB1F3AflTTBVpAP491etrjqCMWx2bBaw3K1vP0Mix6U0vF3J4kP9U\n" + + "eZm6owEA4kX9VAGESvLgIc7CEiswmxdWjxnLQyCRtWXfjgFmYQucWARja86SFgkr\n" + + "BgEEAdpHDwEBB0DBslhDpWC6CV3xJUSo071NSO5Cf4fgOwOj+QHs8mpFbwABAPkQ\n" + + "ioSydYiMi04LyfPohyrhhcdJDHallQg+jYHHUb2pEJCI1QQYFgoAfQUCY2vOkgKe\n" + + "AQKbAgUWAgMBAAQLCQgHBRUKCQgLXyAEGRYKAAYFAmNrzpIACgkQiHlkvEXh+f1e\n" + + "ywEA9A2GLU9LxCJxZf2X4qcZY//YJDChIZHPnY0Vaek1DsMBAN1YILrH2rxQeCXj\n" + + "m4bUKfJIRrGt6ZJscwORgNI1dFQFAAoJEIsHUXcB+VNMK3gA/3vvPm57JsHA860w\n" + + "lB4D1II71oFNL8TFnJqTAvpSKe1AAP49S4mKB4PE0ElcDo7n+nEYt6ba8IMRDlMo\n" + + "rsH85mUgCw==\n" + + "=EMKf\n" + + "-----END PGP PRIVATE KEY BLOCK-----\n"; + + @Test + public void dearmorSecretKey() throws IOException { + PGPSecretKeyRing secretKey = PGPainless.readKeyRing().secretKeyRing(key); + + pipeStringToStdin(key); + ByteArrayOutputStream dearmored = pipeStdoutToStream(); + assertSuccess(executeCommand("dearmor")); + + assertArrayEquals(secretKey.getEncoded(), dearmored.toByteArray()); + } + + @Test + public void dearmorBrokenArmoredKeyFails() throws IOException { + // contains a "-" + String invalidBase64 = "lFgEY2vOkhYJKwYBBAHaRw8BAQdAqGOtLd1tKnuwaYYcdr2/7C0cPiCCggRMKG+Wt32QQdEAAP9VaBzjk/AaAqyykZnQHmS1HByEvRLv5/4yJMSr22451BFjtBRhbGljZUBwZ3BhaW5sZXNzLm9yZ4iOBBMWCgBBBQJja86SCRCLB1F3AflTTBYhBGLp3aTyD4NB0rxLT-IsHUXcB+VNMAp4BApsBBRYCAwEABAsJCAcFFQoJCAsCmQEAACZhAP4s8hn/RBDvyLvGROOd15EYATnWlgyi+b5WXP6cELalJwD1FZy3RROhfNtZWcJPS43fG03pYNyb0NXoitIMAaXEB5xdBGNrzpISCisGAQQBl1UBBQEBB0CqCcYethOynfni8uRO+r/cZWp9hCLy8pRIExKqzcyEFAMBCAcAAP9sRRLoZkLpDaTNNrtIBovXu2ANhL8keUMWtVcuEHnkQA6iiHUEGBYKAB0FAmNrzpICngECmwwFFgIDAQAECwkIBwUVCgkICwAKCRCLB1F3AflTTBVpAP491etrjqCMWx2bBaw3K1vP0Mix6U0vF3J4kP9UeZm6owEA4kX9VAGESvLgIc7CEiswmxdWjxnLQyCRtWXfjgFmYQucWARja86SFgkrBgEEAdpHDwEBB0DBslhDpWC6CV3xJUSo071NSO5Cf4fgOwOj+QHs8mpFbwABAPkQioSydYiMi04LyfPohyrhhcdJDHallQg+jYHHUb2pEJCI1QQYFgoAfQUCY2vOkgKeAQKbAgUWAgMBAAQLCQgHBRUKCQgLXyAEGRYKAAYFAmNrzpIACgkQiHlkvEXh+f1eywEA9A2GLU9LxCJxZf2X4qcZY//YJDChIZHPnY0Vaek1DsMBAN1YILrH2rxQeCXjm4bUKfJIRrGt6ZJscwORgNI1dFQFAAoJEIsHUXcB+VNMK3gA/3vvPm57JsHA860wlB4D1II71oFNL8TFnJqTAvpSKe1AAP49S4mKB4PE0ElcDo7n+nEYt6ba8IMRDlMorsH85mUgCw=="; + pipeStringToStdin(invalidBase64); + ByteArrayOutputStream out = pipeStdoutToStream(); + int exitCode = executeCommand("dearmor"); + + assertEquals(SOPGPException.BadData.EXIT_CODE, exitCode); + assertEquals(0, out.size()); + } + + @Test + public void dearmorCertificate() throws IOException { + PGPSecretKeyRing secretKey = PGPainless.readKeyRing().secretKeyRing(key); + PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKey); + String armoredCert = PGPainless.asciiArmor(certificate); + + pipeStringToStdin(armoredCert); + ByteArrayOutputStream out = pipeStdoutToStream(); + assertSuccess(executeCommand("dearmor")); + + assertArrayEquals(certificate.getEncoded(), out.toByteArray()); + } + + @Test + public void dearmorMessage() throws IOException { + String armored = "-----BEGIN PGP MESSAGE-----\n" + + "Version: BCPG v1.69\n" + + "\n" + + "SGVsbG8sIFdvcmxkCg==\n" + + "=fkLo\n" + + "-----END PGP MESSAGE-----"; + + pipeStringToStdin(armored); + ByteArrayOutputStream out = pipeStdoutToStream(); + assertSuccess(executeCommand("dearmor")); + + assertEquals("Hello, World\n", out.toString()); + } + + @Test + public void dearmorGarbageEmitsEmpty() throws IOException { + String noArmoredData = "This is not armored."; + pipeStringToStdin(noArmoredData); + ByteArrayOutputStream out = pipeStdoutToStream(); + assertSuccess(executeCommand("dearmor")); + assertTrue(out.toString().isEmpty()); + } + +} diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/DearmorTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/DearmorTest.java deleted file mode 100644 index ab5e2c7a..00000000 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/DearmorTest.java +++ /dev/null @@ -1,91 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.cli.commands; - -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.PrintStream; -import java.nio.charset.StandardCharsets; -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; - -import com.ginsberg.junit.exit.FailOnSystemExit; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.pgpainless.PGPainless; -import org.pgpainless.cli.PGPainlessCLI; - -public class DearmorTest { - - private PrintStream originalSout; - - @BeforeEach - public void saveSout() { - this.originalSout = System.out; - } - - @AfterEach - public void restoreSout() { - System.setOut(originalSout); - } - - @Test - @FailOnSystemExit - public void dearmorSecretKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { - PGPSecretKeyRing secretKey = PGPainless.generateKeyRing() - .modernKeyRing("alice@pgpainless.org", null); - String armored = PGPainless.asciiArmor(secretKey); - - System.setIn(new ByteArrayInputStream(armored.getBytes(StandardCharsets.UTF_8))); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - System.setOut(new PrintStream(out)); - PGPainlessCLI.execute("dearmor"); - - assertArrayEquals(secretKey.getEncoded(), out.toByteArray()); - } - - - @Test - @FailOnSystemExit - public void dearmorCertificate() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { - PGPSecretKeyRing secretKey = PGPainless.generateKeyRing() - .modernKeyRing("alice@pgpainless.org", null); - PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKey); - String armored = PGPainless.asciiArmor(certificate); - - System.setIn(new ByteArrayInputStream(armored.getBytes(StandardCharsets.UTF_8))); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - System.setOut(new PrintStream(out)); - PGPainlessCLI.execute("dearmor"); - - assertArrayEquals(certificate.getEncoded(), out.toByteArray()); - } - - @Test - @FailOnSystemExit - public void dearmorMessage() { - String armored = "-----BEGIN PGP MESSAGE-----\n" + - "Version: BCPG v1.69\n" + - "\n" + - "SGVsbG8sIFdvcmxkCg==\n" + - "=fkLo\n" + - "-----END PGP MESSAGE-----"; - - System.setIn(new ByteArrayInputStream(armored.getBytes(StandardCharsets.UTF_8))); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - System.setOut(new PrintStream(out)); - PGPainlessCLI.execute("dearmor"); - - assertEquals("Hello, World\n", out.toString()); - } -} diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/DetachInbandSignatureAndMessageTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/DetachInbandSignatureAndMessageTest.java deleted file mode 100644 index 15cf2546..00000000 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/DetachInbandSignatureAndMessageTest.java +++ /dev/null @@ -1,193 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.cli.commands; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.PrintStream; -import java.nio.charset.StandardCharsets; - -import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; -import org.bouncycastle.util.io.Streams; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.pgpainless.cli.PGPainlessCLI; -import org.pgpainless.cli.TestUtils; -import sop.exception.SOPGPException; - -public class DetachInbandSignatureAndMessageTest { - - private PrintStream originalSout; - private static File tempDir; - private static File certFile; - - private static final String CERT = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + - "Version: BCPG v1.64\n" + - "\n" + - "mFIEXhtfCBMIKoZIzj0DAQcCAwTGSFMBUOSLusXS8hdNHbdK3gN8hS7jd4ky7Czl\n" + - "mSti+oVyRJUwQAFZJ1NMsg1H8flSJP1/9YbHd9FBU4bHKGKPtBE8ZW1pbEBlbWFp\n" + - "bC51c2VyPoh1BBMTCgAdBQJeG18IAhsjBRYCAwEABAsJCAcFFQoJCAsCHgEACgkQ\n" + - "VzbmkxrPNwz8rAD/S/VCQc5NJLArgTDkgrt3Q573HiYfrIQo1uk3dwV15WIBAMiq\n" + - "oDmRMb8jzOBv6FGW4P5WAubPdnAvDD7XmArD+TSeuFYEXhtfCBIIKoZIzj0DAQcC\n" + - "AwTgWDWmHJLQUQ35Qg/rINmUhkUhj1E4O5t6Y2PipbqlGfDufLmIKnX40BoJPS4G\n" + - "HW7U0QXfwSaTXa1BAaNsMUomAwEIB4h1BBgTCgAdBQJeG18IAhsMBRYCAwEABAsJ\n" + - "CAcFFQoJCAsCHgEACgkQVzbmkxrPNwxOcwEA19Fnhw7XwpQoT61Fqg54vroAwTZ3\n" + - "T5A+LOdevAtzNOUA/RWeKfOGk6D+vKYRNpMJyqsHi/vBeKwXoeN0n6HuExVF\n" + - "=a1W7\n" + - "-----END PGP PUBLIC KEY BLOCK-----"; - - @BeforeAll - public static void createTempDir() throws IOException { - tempDir = TestUtils.createTempDirectory(); - - certFile = new File(tempDir, "cert.asc"); - assertTrue(certFile.createNewFile()); - try (FileOutputStream out = new FileOutputStream(certFile)) { - ByteArrayInputStream in = new ByteArrayInputStream(CERT.getBytes(StandardCharsets.UTF_8)); - Streams.pipeAll(in, out); - } - } - - @BeforeEach - public void saveSout() { - this.originalSout = System.out; - } - - @AfterEach - public void restoreSout() { - System.setOut(originalSout); - } - - private static final String CLEAR_SIGNED_MESSAGE = "-----BEGIN PGP SIGNED MESSAGE-----\n" + - "Hash: SHA512\n" + - "\n" + - "Ah, Juliet, if the measure of thy joy\n" + - "Be heaped like mine, and that thy skill be more\n" + - "To blazon it, then sweeten with thy breath\n" + - "This neighbor air, and let rich music’s tongue\n" + - "Unfold the imagined happiness that both\n" + - "Receive in either by this dear encounter.\n" + - "-----BEGIN PGP SIGNATURE-----\n" + - "\n" + - "iHUEARMKAB0WIQRPZlxNwsRmC8ZCXkFXNuaTGs83DAUCYJ/x5gAKCRBXNuaTGs83\n" + - "DFRwAP9/4wMvV3WcX59Clo7mkRce6iwW3VBdiN+yMu3tjmHB2wD/RfE28Q1v4+eo\n" + - "ySNgbyvqYYsNr0fnBwaG3aaj+u5ExiE=\n" + - "=Z2SO\n" + - "-----END PGP SIGNATURE-----"; - - private static final String CLEAR_SIGNED_SIGNATURE = "-----BEGIN PGP SIGNATURE-----\n" + - "\n" + - "iHUEARMKAB0WIQRPZlxNwsRmC8ZCXkFXNuaTGs83DAUCYJ/x5gAKCRBXNuaTGs83\n" + - "DFRwAP9/4wMvV3WcX59Clo7mkRce6iwW3VBdiN+yMu3tjmHB2wD/RfE28Q1v4+eo\n" + - "ySNgbyvqYYsNr0fnBwaG3aaj+u5ExiE=\n" + - "=Z2SO\n" + - "-----END PGP SIGNATURE-----"; - - private static final String CLEAR_SIGNED_BODY = "Ah, Juliet, if the measure of thy joy\n" + - "Be heaped like mine, and that thy skill be more\n" + - "To blazon it, then sweeten with thy breath\n" + - "This neighbor air, and let rich music’s tongue\n" + - "Unfold the imagined happiness that both\n" + - "Receive in either by this dear encounter."; - - @Test - public void detachInbandSignatureAndMessage() throws IOException { - // Clearsigned In - ByteArrayInputStream clearSignedIn = new ByteArrayInputStream(CLEAR_SIGNED_MESSAGE.getBytes(StandardCharsets.UTF_8)); - System.setIn(clearSignedIn); - - // Plaintext Out - ByteArrayOutputStream msgOut = new ByteArrayOutputStream(); - System.setOut(new PrintStream(msgOut)); - - // Detach - File tempSigFile = new File(tempDir, "sig.out"); - PGPainlessCLI.main(new String[] {"detach-inband-signature-and-message", "--signatures-out=" + tempSigFile.getAbsolutePath()}); - - // Test equality with expected values - assertEquals(CLEAR_SIGNED_BODY, msgOut.toString()); - try (FileInputStream sigIn = new FileInputStream(tempSigFile)) { - ByteArrayOutputStream sigBytes = new ByteArrayOutputStream(); - Streams.pipeAll(sigIn, sigBytes); - String sig = sigBytes.toString(); - TestUtils.assertSignatureIsArmored(sigBytes.toByteArray()); - TestUtils.assertSignatureEquals(CLEAR_SIGNED_SIGNATURE, sig); - } catch (FileNotFoundException e) { - fail("Signature File must have been written.", e); - } - - // Check if produced signature still checks out - System.setIn(new ByteArrayInputStream(msgOut.toByteArray())); - ByteArrayOutputStream verifyOut = new ByteArrayOutputStream(); - System.setOut(new PrintStream(verifyOut)); - PGPainlessCLI.main(new String[] {"verify", tempSigFile.getAbsolutePath(), certFile.getAbsolutePath()}); - - assertEquals("2021-05-15T16:08:06Z 4F665C4DC2C4660BC6425E415736E6931ACF370C 4F665C4DC2C4660BC6425E415736E6931ACF370C\n", verifyOut.toString()); - } - - @Test - public void detachInbandSignatureAndMessageNoArmor() throws IOException { - // Clearsigned In - ByteArrayInputStream clearSignedIn = new ByteArrayInputStream(CLEAR_SIGNED_MESSAGE.getBytes(StandardCharsets.UTF_8)); - System.setIn(clearSignedIn); - - // Plaintext Out - ByteArrayOutputStream msgOut = new ByteArrayOutputStream(); - System.setOut(new PrintStream(msgOut)); - - // Detach - File tempSigFile = new File(tempDir, "sig.asc"); - PGPainlessCLI.main(new String[] {"detach-inband-signature-and-message", "--signatures-out=" + tempSigFile.getAbsolutePath(), "--no-armor"}); - - // Test equality with expected values - assertEquals(CLEAR_SIGNED_BODY, msgOut.toString()); - try (FileInputStream sigIn = new FileInputStream(tempSigFile)) { - ByteArrayOutputStream sigBytes = new ByteArrayOutputStream(); - Streams.pipeAll(sigIn, sigBytes); - byte[] sig = sigBytes.toByteArray(); - TestUtils.assertSignatureIsNotArmored(sig); - TestUtils.assertSignatureEquals(CLEAR_SIGNED_SIGNATURE.getBytes(StandardCharsets.UTF_8), sig); - } catch (FileNotFoundException e) { - fail("Signature File must have been written.", e); - } - - // Check if produced signature still checks out - System.setIn(new ByteArrayInputStream(msgOut.toByteArray())); - ByteArrayOutputStream verifyOut = new ByteArrayOutputStream(); - System.setOut(new PrintStream(verifyOut)); - PGPainlessCLI.main(new String[] {"verify", tempSigFile.getAbsolutePath(), certFile.getAbsolutePath()}); - - assertEquals("2021-05-15T16:08:06Z 4F665C4DC2C4660BC6425E415736E6931ACF370C 4F665C4DC2C4660BC6425E415736E6931ACF370C\n", verifyOut.toString()); - } - - @Test - @ExpectSystemExitWithStatus(SOPGPException.OutputExists.EXIT_CODE) - public void existingSignatureOutCausesException() throws IOException { - // Clearsigned In - ByteArrayInputStream clearSignedIn = new ByteArrayInputStream(CLEAR_SIGNED_MESSAGE.getBytes(StandardCharsets.UTF_8)); - System.setIn(clearSignedIn); - - // Plaintext Out - ByteArrayOutputStream msgOut = new ByteArrayOutputStream(); - System.setOut(new PrintStream(msgOut)); - - // Detach - File existingSigFile = new File(tempDir, "sig.existing"); - assertTrue(existingSigFile.createNewFile()); - PGPainlessCLI.main(new String[] {"detach-inband-signature-and-message", "--signatures-out=" + existingSigFile.getAbsolutePath()}); - } - -} diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/EncryptDecryptTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/EncryptDecryptTest.java deleted file mode 100644 index 4e655864..00000000 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/EncryptDecryptTest.java +++ /dev/null @@ -1,115 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.cli.commands; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.io.PrintStream; -import java.nio.charset.StandardCharsets; - -import com.ginsberg.junit.exit.FailOnSystemExit; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.pgpainless.cli.PGPainlessCLI; -import org.pgpainless.cli.TestUtils; - -public class EncryptDecryptTest { - - private static File tempDir; - private static PrintStream originalSout; - - @BeforeAll - public static void prepare() throws IOException { - tempDir = TestUtils.createTempDirectory(); - } - - @Test - @FailOnSystemExit - public void encryptAndDecryptAMessage() throws IOException { - originalSout = System.out; - File julietKeyFile = new File(tempDir, "juliet.key"); - assertTrue(julietKeyFile.createNewFile()); - - File julietCertFile = new File(tempDir, "juliet.asc"); - assertTrue(julietCertFile.createNewFile()); - - File romeoKeyFile = new File(tempDir, "romeo.key"); - assertTrue(romeoKeyFile.createNewFile()); - - File romeoCertFile = new File(tempDir, "romeo.asc"); - assertTrue(romeoCertFile.createNewFile()); - - File msgAscFile = new File(tempDir, "msg.asc"); - assertTrue(msgAscFile.createNewFile()); - - OutputStream julietKeyOut = new FileOutputStream(julietKeyFile); - System.setOut(new PrintStream(julietKeyOut)); - PGPainlessCLI.execute("generate-key", "Juliet Capulet "); - julietKeyOut.close(); - - FileInputStream julietKeyIn = new FileInputStream(julietKeyFile); - System.setIn(julietKeyIn); - OutputStream julietCertOut = new FileOutputStream(julietCertFile); - System.setOut(new PrintStream(julietCertOut)); - PGPainlessCLI.execute("extract-cert"); - julietKeyIn.close(); - julietCertOut.close(); - - OutputStream romeoKeyOut = new FileOutputStream(romeoKeyFile); - System.setOut(new PrintStream(romeoKeyOut)); - PGPainlessCLI.execute("generate-key", "Romeo Montague "); - romeoKeyOut.close(); - - FileInputStream romeoKeyIn = new FileInputStream(romeoKeyFile); - System.setIn(romeoKeyIn); - OutputStream romeoCertOut = new FileOutputStream(romeoCertFile); - System.setOut(new PrintStream(romeoCertOut)); - PGPainlessCLI.execute("extract-cert"); - romeoKeyIn.close(); - romeoCertOut.close(); - - String msg = "Hello World!\n"; - ByteArrayInputStream msgIn = new ByteArrayInputStream(msg.getBytes(StandardCharsets.UTF_8)); - System.setIn(msgIn); - OutputStream msgAscOut = new FileOutputStream(msgAscFile); - System.setOut(new PrintStream(msgAscOut)); - PGPainlessCLI.execute("encrypt", - "--sign-with", romeoKeyFile.getAbsolutePath(), - julietCertFile.getAbsolutePath()); - msgAscOut.close(); - - File verifyFile = new File(tempDir, "verify.txt"); - - FileInputStream msgAscIn = new FileInputStream(msgAscFile); - System.setIn(msgAscIn); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - PrintStream pOut = new PrintStream(out); - System.setOut(pOut); - PGPainlessCLI.execute("decrypt", - "--verify-out", verifyFile.getAbsolutePath(), - "--verify-with", romeoCertFile.getAbsolutePath(), - julietKeyFile.getAbsolutePath()); - msgAscIn.close(); - - assertEquals(msg, out.toString()); - } - - @AfterAll - public static void after() { - System.setOut(originalSout); - // CHECKSTYLE:OFF - System.out.println(tempDir.getAbsolutePath()); - // CHECKSTYLE:ON - } -} diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ExtractCertCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ExtractCertCmdTest.java new file mode 100644 index 00000000..f1f69912 --- /dev/null +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ExtractCertCmdTest.java @@ -0,0 +1,100 @@ +// SPDX-FileCopyrightText: 2021 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.cli.commands; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.key.info.KeyRingInfo; +import org.slf4j.LoggerFactory; +import sop.exception.SOPGPException; + +public class ExtractCertCmdTest extends CLITest { + + public ExtractCertCmdTest() { + super(LoggerFactory.getLogger(ExtractCertCmdTest.class)); + } + + @Test + public void testExtractCert() + throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException, IOException { + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() + .simpleEcKeyRing("Juliet Capulet "); + + pipeBytesToStdin(secretKeys.getEncoded()); + ByteArrayOutputStream out = pipeStdoutToStream(); + assertSuccess(executeCommand("extract-cert", "--armor")); + + assertTrue(out.toString().startsWith("-----BEGIN PGP PUBLIC KEY BLOCK-----\n")); + + PGPPublicKeyRing publicKeys = PGPainless.readKeyRing().publicKeyRing(out.toByteArray()); + KeyRingInfo info = PGPainless.inspectKeyRing(publicKeys); + assertFalse(info.isSecretKey()); + assertTrue(info.isUserIdValid("Juliet Capulet ")); + } + + @Test + public void testExtractCertFromCertFails() throws IOException { + // Generate key + File keyFile = pipeStdoutToFile("key.asc"); + assertSuccess(executeCommand("generate-key", "Alice ")); + + // extract cert from key (success) + pipeFileToStdin(keyFile); + File certFile = pipeStdoutToFile("cert.asc"); + assertSuccess(executeCommand("extract-cert")); + + // extract cert from cert (fail) + pipeFileToStdin(certFile); + ByteArrayOutputStream out = pipeStdoutToStream(); + int exitCode = executeCommand("extract-cert"); + + assertEquals(SOPGPException.BadData.EXIT_CODE, exitCode); + assertEquals(0, out.size()); + } + + @Test + public void extractCertFromGarbageFails() throws IOException { + pipeStringToStdin("This is a bunch of garbage!"); + ByteArrayOutputStream out = pipeStdoutToStream(); + int exitCode = executeCommand("extract-cert"); + + assertEquals(SOPGPException.BadData.EXIT_CODE, exitCode); + assertEquals(0, out.size()); + } + + @Test + public void testExtractCertUnarmored() throws IOException { + // Generate key + File keyFile = pipeStdoutToFile("key.asc"); + assertSuccess(executeCommand("generate-key", "Alice ")); + + // extract cert from key (success) + pipeFileToStdin(keyFile); + ByteArrayOutputStream out = pipeStdoutToStream(); + assertSuccess(executeCommand("extract-cert", "--no-armor")); + + assertFalse(out.toString().startsWith("-----BEGIN PGP PUBLIC KEY BLOCK-----\n")); + + pipeBytesToStdin(out.toByteArray()); + ByteArrayOutputStream armored = pipeStdoutToStream(); + assertSuccess(executeCommand("armor")); + + assertTrue(armored.toString().startsWith("-----BEGIN PGP PUBLIC KEY BLOCK-----\n")); + } + +} diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ExtractCertTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ExtractCertTest.java deleted file mode 100644 index 6f746445..00000000 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ExtractCertTest.java +++ /dev/null @@ -1,45 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.cli.commands; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.PrintStream; -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; - -import com.ginsberg.junit.exit.FailOnSystemExit; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.junit.jupiter.api.Test; -import org.pgpainless.PGPainless; -import org.pgpainless.cli.PGPainlessCLI; -import org.pgpainless.key.info.KeyRingInfo; - -public class ExtractCertTest { - - @Test - @FailOnSystemExit - public void testExtractCert() throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException, IOException { - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() - .simpleEcKeyRing("Juliet Capulet "); - ByteArrayInputStream inputStream = new ByteArrayInputStream(secretKeys.getEncoded()); - System.setIn(inputStream); - - ByteArrayOutputStream out = new ByteArrayOutputStream(); - System.setOut(new PrintStream(out)); - - PGPainlessCLI.execute("extract-cert"); - PGPPublicKeyRing publicKeys = PGPainless.readKeyRing().publicKeyRing(out.toByteArray()); - KeyRingInfo info = PGPainless.inspectKeyRing(publicKeys); - assertFalse(info.isSecretKey()); - assertTrue(info.isUserIdValid("Juliet Capulet ")); - } -} diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/GenerateCertTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/GenerateCertTest.java deleted file mode 100644 index 87ce74a4..00000000 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/GenerateCertTest.java +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.cli.commands; - -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.pgpainless.cli.TestUtils.ARMOR_PRIVATE_KEY_HEADER_BYTES; - -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.IOException; -import java.io.PrintStream; -import java.util.Arrays; - -import com.ginsberg.junit.exit.FailOnSystemExit; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.pgpainless.PGPainless; -import org.pgpainless.cli.PGPainlessCLI; -import org.pgpainless.cli.TestUtils; -import org.pgpainless.key.info.KeyRingInfo; - -public class GenerateCertTest { - - private static File tempDir; - - - @BeforeAll - public static void setup() throws IOException { - tempDir = TestUtils.createTempDirectory(); - } - - @Test - @FailOnSystemExit - public void testKeyGeneration() throws IOException, PGPException { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - System.setOut(new PrintStream(out)); - PGPainlessCLI.execute("generate-key", "--armor", "Juliet Capulet "); - - PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(out.toByteArray()); - KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); - assertTrue(info.isUserIdValid("Juliet Capulet ")); - - byte[] outBegin = new byte[37]; - System.arraycopy(out.toByteArray(), 0, outBegin, 0, 37); - assertArrayEquals(outBegin, ARMOR_PRIVATE_KEY_HEADER_BYTES); - } - - @Test - @FailOnSystemExit - public void testNoArmor() { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - System.setOut(new PrintStream(out)); - PGPainlessCLI.execute("generate-key", "--no-armor", "Test "); - - byte[] outBegin = new byte[37]; - System.arraycopy(out.toByteArray(), 0, outBegin, 0, 37); - assertFalse(Arrays.equals(outBegin, ARMOR_PRIVATE_KEY_HEADER_BYTES)); - } -} diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/GenerateKeyCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/GenerateKeyCmdTest.java new file mode 100644 index 00000000..302e4b8f --- /dev/null +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/GenerateKeyCmdTest.java @@ -0,0 +1,98 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.cli.commands; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.util.encoders.Hex; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.key.info.KeyRingInfo; +import org.pgpainless.key.protection.UnlockSecretKey; +import org.pgpainless.util.Passphrase; +import org.slf4j.LoggerFactory; +import sop.exception.SOPGPException; + +public class GenerateKeyCmdTest extends CLITest { + + public GenerateKeyCmdTest() { + super(LoggerFactory.getLogger(GenerateKeyCmdTest.class)); + } + + @Test + public void testGenerateKey() throws IOException { + File keyFile = pipeStdoutToFile("key.asc"); + assertSuccess(executeCommand("generate-key", "Alice ")); + + String key = readStringFromFile(keyFile); + assertTrue(key.startsWith("-----BEGIN PGP PRIVATE KEY BLOCK-----\n")); + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(key); + KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); + assertTrue(info.isFullyDecrypted()); + assertEquals(Collections.singletonList("Alice "), info.getUserIds()); + } + + @Test + public void testGenerateBinaryKey() throws IOException { + ByteArrayOutputStream out = pipeStdoutToStream(); + assertSuccess(executeCommand("generate-key", "--no-armor", + "Alice ")); + + byte[] key = out.toByteArray(); + String firstHexOctet = Hex.toHexString(key, 0, 1); + assertTrue(firstHexOctet.equals("c5") || firstHexOctet.equals("94")); + } + + @Test + public void testGenerateKeyWithMultipleUserIds() throws IOException { + ByteArrayOutputStream out = pipeStdoutToStream(); + assertSuccess(executeCommand("generate-key", + "Alice ", "Alice ")); + + String key = out.toString(); + assertTrue(key.startsWith("-----BEGIN PGP PRIVATE KEY BLOCK-----\n")); + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(key); + KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); + assertTrue(info.isFullyDecrypted()); + assertEquals(Arrays.asList("Alice ", "Alice "), info.getUserIds()); + } + + @Test + public void testPasswordProtectedKey() throws IOException, PGPException { + File passwordFile = writeFile("password", "sw0rdf1sh"); + passwordFile.deleteOnExit(); + ByteArrayOutputStream out = pipeStdoutToStream(); + assertSuccess(executeCommand("generate-key", + "--with-key-password", passwordFile.getAbsolutePath(), "Alice ")); + + String key = out.toString(); + assertTrue(key.startsWith("-----BEGIN PGP PRIVATE KEY BLOCK-----\n")); + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(key); + KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); + assertTrue(info.isFullyEncrypted()); + + assertNotNull(UnlockSecretKey + .unlockSecretKey(secretKeys.getSecretKey(), Passphrase.fromPassword("sw0rdf1sh"))); + } + + @Test + public void testGeneratePasswordProtectedKey_missingPasswordFile() throws IOException { + int exit = executeCommand("generate-key", + "--with-key-password", "nonexistent", "Alice "); + + assertEquals(SOPGPException.MissingInput.EXIT_CODE, exit, + "Expected MISSING_INPUT (" + SOPGPException.MissingInput.EXIT_CODE + ")"); + } +} diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/InlineDetachCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/InlineDetachCmdTest.java new file mode 100644 index 00000000..19bc9aa5 --- /dev/null +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/InlineDetachCmdTest.java @@ -0,0 +1,155 @@ +// SPDX-FileCopyrightText: 2021 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.cli.commands; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.Test; +import org.pgpainless.cli.TestUtils; +import org.slf4j.LoggerFactory; +import sop.exception.SOPGPException; + +public class InlineDetachCmdTest extends CLITest { + + private static final String CERT = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "Version: BCPG v1.64\n" + + "\n" + + "mFIEXhtfCBMIKoZIzj0DAQcCAwTGSFMBUOSLusXS8hdNHbdK3gN8hS7jd4ky7Czl\n" + + "mSti+oVyRJUwQAFZJ1NMsg1H8flSJP1/9YbHd9FBU4bHKGKPtBE8ZW1pbEBlbWFp\n" + + "bC51c2VyPoh1BBMTCgAdBQJeG18IAhsjBRYCAwEABAsJCAcFFQoJCAsCHgEACgkQ\n" + + "VzbmkxrPNwz8rAD/S/VCQc5NJLArgTDkgrt3Q573HiYfrIQo1uk3dwV15WIBAMiq\n" + + "oDmRMb8jzOBv6FGW4P5WAubPdnAvDD7XmArD+TSeuFYEXhtfCBIIKoZIzj0DAQcC\n" + + "AwTgWDWmHJLQUQ35Qg/rINmUhkUhj1E4O5t6Y2PipbqlGfDufLmIKnX40BoJPS4G\n" + + "HW7U0QXfwSaTXa1BAaNsMUomAwEIB4h1BBgTCgAdBQJeG18IAhsMBRYCAwEABAsJ\n" + + "CAcFFQoJCAsCHgEACgkQVzbmkxrPNwxOcwEA19Fnhw7XwpQoT61Fqg54vroAwTZ3\n" + + "T5A+LOdevAtzNOUA/RWeKfOGk6D+vKYRNpMJyqsHi/vBeKwXoeN0n6HuExVF\n" + + "=a1W7\n" + + "-----END PGP PUBLIC KEY BLOCK-----"; + + private static final String CLEAR_SIGNED_MESSAGE = "-----BEGIN PGP SIGNED MESSAGE-----\n" + + "Hash: SHA512\n" + + "\n" + + "Ah, Juliet, if the measure of thy joy\n" + + "Be heaped like mine, and that thy skill be more\n" + + "To blazon it, then sweeten with thy breath\n" + + "This neighbor air, and let rich music’s tongue\n" + + "Unfold the imagined happiness that both\n" + + "Receive in either by this dear encounter.\n" + + "-----BEGIN PGP SIGNATURE-----\n" + + "\n" + + "iHUEARMKAB0WIQRPZlxNwsRmC8ZCXkFXNuaTGs83DAUCYJ/x5gAKCRBXNuaTGs83\n" + + "DFRwAP9/4wMvV3WcX59Clo7mkRce6iwW3VBdiN+yMu3tjmHB2wD/RfE28Q1v4+eo\n" + + "ySNgbyvqYYsNr0fnBwaG3aaj+u5ExiE=\n" + + "=Z2SO\n" + + "-----END PGP SIGNATURE-----"; + + private static final String CLEAR_SIGNED_SIGNATURE = "-----BEGIN PGP SIGNATURE-----\n" + + "\n" + + "iHUEARMKAB0WIQRPZlxNwsRmC8ZCXkFXNuaTGs83DAUCYJ/x5gAKCRBXNuaTGs83\n" + + "DFRwAP9/4wMvV3WcX59Clo7mkRce6iwW3VBdiN+yMu3tjmHB2wD/RfE28Q1v4+eo\n" + + "ySNgbyvqYYsNr0fnBwaG3aaj+u5ExiE=\n" + + "=Z2SO\n" + + "-----END PGP SIGNATURE-----"; + + private static final String CLEAR_SIGNED_BODY = "Ah, Juliet, if the measure of thy joy\n" + + "Be heaped like mine, and that thy skill be more\n" + + "To blazon it, then sweeten with thy breath\n" + + "This neighbor air, and let rich music’s tongue\n" + + "Unfold the imagined happiness that both\n" + + "Receive in either by this dear encounter."; + + public InlineDetachCmdTest() { + super(LoggerFactory.getLogger(InlineDetachCmdTest.class)); + } + + @Test + public void detachInbandSignatureAndMessage() throws IOException { + pipeStringToStdin(CLEAR_SIGNED_MESSAGE); + ByteArrayOutputStream msgOut = pipeStdoutToStream(); + File sigFile = nonExistentFile("sig.out"); + + assertSuccess(executeCommand("inline-detach", "--signatures-out", sigFile.getAbsolutePath())); + assertTrue(sigFile.exists(), "Signature file must have been written."); + + // Test equality with expected values + assertEquals(CLEAR_SIGNED_BODY, msgOut.toString()); + String sig = readStringFromFile(sigFile); + TestUtils.assertSignatureIsArmored(sig.getBytes()); + TestUtils.assertSignatureEquals(CLEAR_SIGNED_SIGNATURE, sig); + + // Check if produced signature still checks out + File certFile = writeFile("cert.asc", CERT); + pipeStringToStdin(msgOut.toString()); + ByteArrayOutputStream verifyOut = pipeStdoutToStream(); + assertSuccess(executeCommand("verify", sigFile.getAbsolutePath(), certFile.getAbsolutePath())); + assertEquals("2021-05-15T16:08:06Z 4F665C4DC2C4660BC6425E415736E6931ACF370C 4F665C4DC2C4660BC6425E415736E6931ACF370C mode:text\n", + verifyOut.toString()); + } + + @Test + public void detachInbandSignatureAndMessageNoArmor() throws IOException { + pipeStringToStdin(CLEAR_SIGNED_MESSAGE); + ByteArrayOutputStream msgOut = pipeStdoutToStream(); + File sigFile = nonExistentFile("sig.out"); + + assertSuccess(executeCommand("inline-detach", "--signatures-out", sigFile.getAbsolutePath(), "--no-armor")); + + // Test equality with expected values + assertEquals(CLEAR_SIGNED_BODY, msgOut.toString()); + assertTrue(sigFile.exists(), "Signature file must have been written."); + byte[] sig = readBytesFromFile(sigFile); + + TestUtils.assertSignatureIsNotArmored(sig); + TestUtils.assertSignatureEquals(CLEAR_SIGNED_SIGNATURE.getBytes(StandardCharsets.UTF_8), sig); + + // Check if produced signature still checks out + pipeBytesToStdin(msgOut.toByteArray()); + ByteArrayOutputStream verifyOut = pipeStdoutToStream(); + File certFile = writeFile("cert.asc", CERT); + assertSuccess(executeCommand("verify", sigFile.getAbsolutePath(), certFile.getAbsolutePath())); + assertEquals("2021-05-15T16:08:06Z 4F665C4DC2C4660BC6425E415736E6931ACF370C 4F665C4DC2C4660BC6425E415736E6931ACF370C mode:text\n", + verifyOut.toString()); + } + + @Test + public void existingSignatureOutCausesException() throws IOException { + pipeStringToStdin(CLEAR_SIGNED_MESSAGE); + ByteArrayOutputStream msgOut = pipeStdoutToStream(); + File existingSigFile = writeFile("sig.asc", CLEAR_SIGNED_SIGNATURE); + int exit = executeCommand("inline-detach", "--signatures-out", existingSigFile.getAbsolutePath()); + assertEquals(SOPGPException.OutputExists.EXIT_CODE, exit); + assertEquals(0, msgOut.size()); + } + + @Test + public void detachNonOpenPgpDataFails() throws IOException { + File sig = nonExistentFile("sig.asc"); + pipeStringToStdin("This is non-OpenPGP data and therefore we cannot detach any signatures from it."); + int exitCode = executeCommand("inline-detach", "--signatures-out", sig.getAbsolutePath()); + + assertEquals(SOPGPException.BadData.EXIT_CODE, exitCode); + } + + @Test + public void detachMissingSignaturesFromCleartextSignedMessageFails() throws IOException { + String cleartextSignedNoSigs = "-----BEGIN PGP SIGNED MESSAGE-----\n" + + "\n" + + "Hello, World!\n" + + "What's Up!??\n" + + "\n" + + "\n"; + pipeStringToStdin(cleartextSignedNoSigs); + File sig = nonExistentFile("sig.asc"); + int exitCode = executeCommand("inline-detach", "--signatures-out", sig.getAbsolutePath()); + + assertEquals(SOPGPException.BadData.EXIT_CODE, exitCode); + } +} diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ListProfilesCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ListProfilesCmdTest.java new file mode 100644 index 00000000..b3e3cac3 --- /dev/null +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ListProfilesCmdTest.java @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.cli.commands; + +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import org.junit.jupiter.api.Test; +import org.slf4j.LoggerFactory; + +public class ListProfilesCmdTest extends CLITest { + + public ListProfilesCmdTest() { + super(LoggerFactory.getLogger(ListProfilesCmdTest.class)); + } + + @Test + public void listProfilesWithoutCommand() throws IOException { + assertNotEquals(0, executeCommand("list-profiles")); + } + + @Test + public void listProfileOfGenerateKey() throws IOException { + ByteArrayOutputStream output = pipeStdoutToStream(); + assertSuccess(executeCommand("list-profiles", "generate-key")); + + assertTrue(output.toString().contains("rfc4880")); + } + + @Test + public void listProfilesOfEncrypt() throws IOException { + ByteArrayOutputStream output = pipeStdoutToStream(); + assertSuccess(executeCommand("list-profiles", "encrypt")); + + assertTrue(output.toString().contains("rfc4880")); + } +} diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripEncryptDecryptCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripEncryptDecryptCmdTest.java new file mode 100644 index 00000000..f8d56bc3 --- /dev/null +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripEncryptDecryptCmdTest.java @@ -0,0 +1,673 @@ +// SPDX-FileCopyrightText: 2021 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.cli.commands; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.KeyFlag; +import org.pgpainless.key.generation.KeySpec; +import org.pgpainless.key.generation.type.KeyType; +import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve; +import org.pgpainless.key.generation.type.xdh_legacy.XDHLegacySpec; +import org.slf4j.LoggerFactory; +import sop.exception.SOPGPException; + +public class RoundTripEncryptDecryptCmdTest extends CLITest { + + public RoundTripEncryptDecryptCmdTest() { + super(LoggerFactory.getLogger(RoundTripEncryptDecryptCmdTest.class)); + } + + private static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: A2EC 077F C977 E15D D799 EFF9 2C0D 3C12 3CF5 1C08\n" + + "Comment: Alice \n" + + "\n" + + "lFgEY2veRhYJKwYBBAHaRw8BAQdAeJYBoCcnGPQ3nchyyBrWQ83q3hqJnfZn2mqh\n" + + "d1M7WwsAAP0R1ELnfdJhXcfjaYPLHzwy1i34FxP5g3tvdgg9Q7VmchActBxBbGlj\n" + + "ZSA8YWxpY2VAcGdwYWlubGVzcy5vcmc+iI8EExYKAEEFAmNr3kYJECwNPBI89RwI\n" + + "FiEEouwHf8l34V3Xme/5LA08Ejz1HAgCngECmwEFFgIDAQAECwkIBwUVCgkICwKZ\n" + + "AQAAe6YA/2sO483Vi2Fgs4ejv8FykyO96IVrMoYhw3Od4LyWEyDfAQDi15LxJJm6\n" + + "T2sXdENVigdwDJiELxjOtbmivuJutxkWCJxdBGNr3kYSCisGAQQBl1UBBQEBB0CS\n" + + "zXjySHqlicxG3QlrVeTIqwKitL1sWsx0MCDmT1C8dAMBCAcAAP9VNkfMQvYAlYSP\n" + + "aYEkwEOc8/XpiloVKtPzxwVCPlXFeBDCiHUEGBYKAB0FAmNr3kYCngECmwwFFgID\n" + + "AQAECwkIBwUVCgkICwAKCRAsDTwSPPUcCOT4AQDZcN5a/e8Qr+LNBIyXXLgJWGsL\n" + + "59nsKHBbDURnxbEnMQEAybS8u+Rsb82yW4CfaA4CLRTC3eDc5Y4QwYWzLogWNwic\n" + + "WARja95GFgkrBgEEAdpHDwEBB0DcdwQufWLq6ASku4JWBBd9JplRVhK0cXWuTE73\n" + + "uWltuwABAI0bVQXvgDnxTs6kUO7JIWtokM5lI/1bfG4L1YOfnXIgD7CI1QQYFgoA\n" + + "fQUCY2veRgKeAQKbAgUWAgMBAAQLCQgHBRUKCQgLXyAEGRYKAAYFAmNr3kYACgkQ\n" + + "7NC/hj9lyaWVAwEA3ze1LCi1reGfB5tS3Au6A8aalyk4UV0iVOXxwV5r+E4BAJGz\n" + + "ZMFF/iQ/rOcSAsHPp4ggezZALDIkT2Hrn6iLDdsLAAoJECwNPBI89RwIuBIBAMxG\n" + + "u/s4maOFozcO4JoCZTsLHGy70SG6UuVQjK0EyJJ1APoDEfK+qTlC7/FoijMA6Ew9\n" + + "aesZ2IHgpwA7jlyHSgwLDw==\n" + + "=H3HU\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + private static final String CERT = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: A2EC 077F C977 E15D D799 EFF9 2C0D 3C12 3CF5 1C08\n" + + "Comment: Alice \n" + + "\n" + + "mDMEY2veRhYJKwYBBAHaRw8BAQdAeJYBoCcnGPQ3nchyyBrWQ83q3hqJnfZn2mqh\n" + + "d1M7Wwu0HEFsaWNlIDxhbGljZUBwZ3BhaW5sZXNzLm9yZz6IjwQTFgoAQQUCY2ve\n" + + "RgkQLA08Ejz1HAgWIQSi7Ad/yXfhXdeZ7/ksDTwSPPUcCAKeAQKbAQUWAgMBAAQL\n" + + "CQgHBRUKCQgLApkBAAB7pgD/aw7jzdWLYWCzh6O/wXKTI73ohWsyhiHDc53gvJYT\n" + + "IN8BAOLXkvEkmbpPaxd0Q1WKB3AMmIQvGM61uaK+4m63GRYIuDgEY2veRhIKKwYB\n" + + "BAGXVQEFAQEHQJLNePJIeqWJzEbdCWtV5MirAqK0vWxazHQwIOZPULx0AwEIB4h1\n" + + "BBgWCgAdBQJja95GAp4BApsMBRYCAwEABAsJCAcFFQoJCAsACgkQLA08Ejz1HAjk\n" + + "+AEA2XDeWv3vEK/izQSMl1y4CVhrC+fZ7ChwWw1EZ8WxJzEBAMm0vLvkbG/NsluA\n" + + "n2gOAi0Uwt3g3OWOEMGFsy6IFjcIuDMEY2veRhYJKwYBBAHaRw8BAQdA3HcELn1i\n" + + "6ugEpLuCVgQXfSaZUVYStHF1rkxO97lpbbuI1QQYFgoAfQUCY2veRgKeAQKbAgUW\n" + + "AgMBAAQLCQgHBRUKCQgLXyAEGRYKAAYFAmNr3kYACgkQ7NC/hj9lyaWVAwEA3ze1\n" + + "LCi1reGfB5tS3Au6A8aalyk4UV0iVOXxwV5r+E4BAJGzZMFF/iQ/rOcSAsHPp4gg\n" + + "ezZALDIkT2Hrn6iLDdsLAAoJECwNPBI89RwIuBIBAMxGu/s4maOFozcO4JoCZTsL\n" + + "HGy70SG6UuVQjK0EyJJ1APoDEfK+qTlC7/FoijMA6Ew9aesZ2IHgpwA7jlyHSgwL\n" + + "Dw==\n" + + "=c1PZ\n" + + "-----END PGP PUBLIC KEY BLOCK-----"; + + @Test + public void encryptAndDecryptAMessage() throws IOException { + // Juliets key and cert + File julietKeyFile = pipeStdoutToFile("juliet.key"); + assertSuccess(executeCommand("generate-key", "Juliet ")); + + pipeFileToStdin(julietKeyFile); + File julietCertFile = pipeStdoutToFile("juliet.cert"); + assertSuccess(executeCommand("extract-cert")); + + // Romeos key and cert + File romeoKeyFile = pipeStdoutToFile("romeo.key"); + assertSuccess(executeCommand("generate-key", "Romeo ")); + + File romeoCertFile = pipeStdoutToFile("romeo.cert"); + pipeFileToStdin(romeoKeyFile); + assertSuccess(executeCommand("extract-cert")); + + // Romeo encrypts signs and encrypts for Juliet and himself + String msg = "Hello World!\n"; + File encryptedMessageFile = pipeStdoutToFile("msg.asc"); + pipeStringToStdin(msg); + assertSuccess(executeCommand("encrypt", "--sign-with", romeoKeyFile.getAbsolutePath(), + julietCertFile.getAbsolutePath(), romeoCertFile.getAbsolutePath())); + + // Juliet can decrypt and verify with Romeos cert + pipeFileToStdin(encryptedMessageFile); + File verificationsFile = nonExistentFile("verifications"); + ByteArrayOutputStream decrypted = pipeStdoutToStream(); + assertSuccess(executeCommand("decrypt", "--verifications-out", verificationsFile.getAbsolutePath(), + "--verify-with", romeoCertFile.getAbsolutePath(), + julietKeyFile.getAbsolutePath())); + assertEquals(msg, decrypted.toString()); + + // Romeo can decrypt and verify too + pipeFileToStdin(encryptedMessageFile); + File anotherVerificationsFile = nonExistentFile("anotherVerifications"); + decrypted = pipeStdoutToStream(); + assertSuccess(executeCommand("decrypt", "--verifications-out", anotherVerificationsFile.getAbsolutePath(), + "--verify-with", romeoCertFile.getAbsolutePath(), + romeoKeyFile.getAbsolutePath())); + assertEquals(msg, decrypted.toString()); + + String julietsVerif = readStringFromFile(verificationsFile); + String romeosVerif = readStringFromFile(anotherVerificationsFile); + assertEquals(julietsVerif, romeosVerif); + assertFalse(julietsVerif.isEmpty()); + assertEquals(115, julietsVerif.length()); // 115 is number of symbols in [DATE, FINGER, FINGER, MODE] for V4 + } + + @Test + public void testMissingArgumentsIfNoArgsSupplied() throws IOException { + int exit = executeCommand("encrypt"); + assertEquals(SOPGPException.MissingArg.EXIT_CODE, exit); + } + + @Test + @Disabled("Disabled, since we now read certificates from secret keys") + public void testEncryptingForKeyFails() throws IOException { + File notACert = writeFile("key.asc", KEY); + + pipeStringToStdin("Hello, World!"); + ByteArrayOutputStream out = pipeStdoutToStream(); + int exitCode = executeCommand("encrypt", notACert.getAbsolutePath()); + assertEquals(SOPGPException.BadData.EXIT_CODE, exitCode); + assertEquals(0, out.size()); + } + + @Test + public void testEncrypt_SignWithCertFails() throws IOException { + File cert = writeFile("cert.asc", CERT); + // noinspection UnnecessaryLocalVariable + File notAKey = cert; + + pipeStringToStdin("Hello, World!"); + ByteArrayOutputStream out = pipeStdoutToStream(); + int exitCode = executeCommand("encrypt", "--sign-with", notAKey.getAbsolutePath(), cert.getAbsolutePath()); + assertEquals(SOPGPException.BadData.EXIT_CODE, exitCode); + assertEquals(0, out.size()); + } + + @Test + public void testDecryptVerifyOut_withoutVerifyWithFails() throws IOException { + File key = writeFile("key.asc", KEY); + + File verifications = nonExistentFile("verifications"); + ByteArrayOutputStream out = pipeStdoutToStream(); + int exitCode = executeCommand("decrypt", "--verifications-out", + verifications.getAbsolutePath(), key.getAbsolutePath()); + + assertEquals(SOPGPException.IncompleteVerification.EXIT_CODE, exitCode); + assertEquals(0, out.size()); + } + + @Test + public void testVerificationsOutAlreadyExistFails() throws IOException { + File key = writeFile("key.asc", KEY); + File cert = writeFile("cert.asc", CERT); + + File verifications = writeFile("verifications", "this file is not empty"); + ByteArrayOutputStream out = pipeStdoutToStream(); + int exitCode = executeCommand("decrypt", "--verify-with", cert.getAbsolutePath(), + "--verifications-out", verifications.getAbsolutePath(), + key.getAbsolutePath()); + + assertEquals(SOPGPException.OutputExists.EXIT_CODE, exitCode); + assertEquals(0, out.size()); + } + + @Test + public void testSessionKeyOutWritesSessionKeyOut() throws IOException { + File key = writeFile("key.asc", KEY); + File sessionKeyFile = nonExistentFile("session.key"); + + String plaintext = "Hello, World!\n"; + String ciphertext = "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "hF4D831k4umlLu4SAQdAYisjZTDRm217LHQbqjB766tm62CKTkRj3Gd0wYxVRCgw\n" + + "48SnOJINCJoPgDsxk2NiJmLCImoiET7IElqxN9htdDXQJwcRK+71r/ZyO4YJpWuX\n" + + "0sAAAcEFc3nT+un31sOi8KoBJlc5n+MemntQvcWDs8B87BEW/Ncjrs0s4pJpZKBQ\n" + + "/AWc4wLCI3ylfMQJB2pICqaOO3KP3WepgTIw5fuZm6YfriKQi7uZvVx1N+uaCIoa\n" + + "K2IVVf/7O9KZJ9GbsGYdpBj9IdaIZiVS3Xi8rwgQl3haI/EeHC3nnCsWyj23Fjt3\n" + + "LjbMqpHbSnp8U1cQ8rXavrREaKv69PFeJio6/hRg32TzJqn05dPALRxHMEkxxa4h\n" + + "FpVU\n" + + "=edS5\n" + + "-----END PGP MESSAGE-----"; + String sessionKey = "9:B6FAD96B7ED2DA27D8A36EAEA75DAB7AC587180B14D8A24BD7263524F3DDECC3\n"; + + pipeStringToStdin(ciphertext); + ByteArrayOutputStream plaintextOut = pipeStdoutToStream(); + assertSuccess(executeCommand("decrypt", "--session-key-out", + sessionKeyFile.getAbsolutePath(), key.getAbsolutePath())); + + assertEquals(plaintext, plaintextOut.toString()); + String resultSessionKey = readStringFromFile(sessionKeyFile); + assertEquals(sessionKey, resultSessionKey); + } + + @Test + public void decryptMessageWithSessionKey() throws IOException { + String plaintext = "Hello, World!\n"; + String ciphertext = "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "hF4D831k4umlLu4SAQdAYisjZTDRm217LHQbqjB766tm62CKTkRj3Gd0wYxVRCgw\n" + + "48SnOJINCJoPgDsxk2NiJmLCImoiET7IElqxN9htdDXQJwcRK+71r/ZyO4YJpWuX\n" + + "0sAAAcEFc3nT+un31sOi8KoBJlc5n+MemntQvcWDs8B87BEW/Ncjrs0s4pJpZKBQ\n" + + "/AWc4wLCI3ylfMQJB2pICqaOO3KP3WepgTIw5fuZm6YfriKQi7uZvVx1N+uaCIoa\n" + + "K2IVVf/7O9KZJ9GbsGYdpBj9IdaIZiVS3Xi8rwgQl3haI/EeHC3nnCsWyj23Fjt3\n" + + "LjbMqpHbSnp8U1cQ8rXavrREaKv69PFeJio6/hRg32TzJqn05dPALRxHMEkxxa4h\n" + + "FpVU\n" + + "=edS5\n" + + "-----END PGP MESSAGE-----"; + String sessionKey = "9:B6FAD96B7ED2DA27D8A36EAEA75DAB7AC587180B14D8A24BD7263524F3DDECC3\n"; + + File sessionKeyFile = writeFile("session.key", sessionKey); + + pipeStringToStdin(ciphertext); + ByteArrayOutputStream plaintextOut = pipeStdoutToStream(); + assertSuccess(executeCommand("decrypt", "--with-session-key", sessionKeyFile.getAbsolutePath())); + + assertEquals(plaintext, plaintextOut.toString()); + } + + @Test + public void testDecryptWithSessionKeyVerifyWithYieldsExpectedVerifications() throws IOException { + String plaintext = "Hello, World!\n"; + String ciphertext = "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "hF4D831k4umlLu4SAQdAYisjZTDRm217LHQbqjB766tm62CKTkRj3Gd0wYxVRCgw\n" + + "48SnOJINCJoPgDsxk2NiJmLCImoiET7IElqxN9htdDXQJwcRK+71r/ZyO4YJpWuX\n" + + "0sAAAcEFc3nT+un31sOi8KoBJlc5n+MemntQvcWDs8B87BEW/Ncjrs0s4pJpZKBQ\n" + + "/AWc4wLCI3ylfMQJB2pICqaOO3KP3WepgTIw5fuZm6YfriKQi7uZvVx1N+uaCIoa\n" + + "K2IVVf/7O9KZJ9GbsGYdpBj9IdaIZiVS3Xi8rwgQl3haI/EeHC3nnCsWyj23Fjt3\n" + + "LjbMqpHbSnp8U1cQ8rXavrREaKv69PFeJio6/hRg32TzJqn05dPALRxHMEkxxa4h\n" + + "FpVU\n" + + "=edS5\n" + + "-----END PGP MESSAGE-----"; + String sessionKey = "9:B6FAD96B7ED2DA27D8A36EAEA75DAB7AC587180B14D8A24BD7263524F3DDECC3\n"; + + File cert = writeFile("cert.asc", CERT); + File sessionKeyFile = writeFile("session.key", sessionKey); + File verifications = nonExistentFile("verifications"); + + pipeStringToStdin(ciphertext); + ByteArrayOutputStream out = pipeStdoutToStream(); + assertSuccess(executeCommand("decrypt", "--with-session-key", sessionKeyFile.getAbsolutePath(), + "--verify-with", cert.getAbsolutePath(), "--verifications-out", verifications.getAbsolutePath())); + + assertEquals(plaintext, out.toString()); + String verificationString = readStringFromFile(verifications); + assertEquals("2022-11-09T17:22:48Z C0DCEC44B1A173664B05DABCECD0BF863F65C9A5 A2EC077FC977E15DD799EFF92C0D3C123CF51C08 mode:binary\n", + verificationString); + } + + @Test + public void encryptAndDecryptMessageWithPassphrase() throws IOException { + File passwordFile = writeFile("password", "c1tiz€n4"); + String message = "I cannot think of meaningful messages for test vectors rn"; + + pipeStringToStdin(message); + ByteArrayOutputStream ciphertext = pipeStdoutToStream(); + assertSuccess(executeCommand("encrypt", "--with-password", passwordFile.getAbsolutePath())); + + String ciphertextString = ciphertext.toString(); + assertTrue(ciphertextString.startsWith("-----BEGIN PGP MESSAGE-----\n")); + + pipeBytesToStdin(ciphertext.toByteArray()); + ByteArrayOutputStream plaintext = pipeStdoutToStream(); + assertSuccess(executeCommand("decrypt", "--with-password", passwordFile.getAbsolutePath())); + + assertEquals(message, plaintext.toString()); + } + + @Test + public void testEncryptWithIncapableCert() throws PGPException, + InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() + .addUserId("No Crypt ") + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), + KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA)) + .build(); + PGPPublicKeyRing cert = PGPainless.extractCertificate(secretKeys); + File certFile = writeFile("cert.pgp", cert.getEncoded()); + + pipeStringToStdin("Hello, World!\n"); + ByteArrayOutputStream out = pipeStdoutToStream(); + int exitCode = executeCommand("encrypt", certFile.getAbsolutePath()); + + assertEquals(SOPGPException.CertCannotEncrypt.EXIT_CODE, exitCode); + assertEquals(0, out.size()); + } + + @Test + public void testSignWithIncapableKey() + throws IOException, PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() + .addUserId("Cannot Sign ") + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER)) + .addSubkey(KeySpec.getBuilder( + KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)) + .build(); + File keyFile = writeFile("key.pgp", secretKeys.getEncoded()); + File certFile = writeFile("cert.pgp", PGPainless.extractCertificate(secretKeys).getEncoded()); + + pipeStringToStdin("Hello, World!\n"); + ByteArrayOutputStream out = pipeStdoutToStream(); + int exitCode = executeCommand("encrypt", "--sign-with", keyFile.getAbsolutePath(), + certFile.getAbsolutePath()); + + assertEquals(SOPGPException.KeyCannotSign.EXIT_CODE, exitCode); + assertEquals(0, out.size()); + } + + @Test + public void testEncryptDecryptRoundTripWithPasswordProtectedKey() throws IOException { + // generate password protected key + File passwordFile = writeFile("password", "fooBarBaz420"); + File keyFile = pipeStdoutToFile("key.asc"); + assertSuccess(executeCommand("generate-key", + "--with-key-password", passwordFile.getAbsolutePath(), + "Pascal Password ")); + + // extract cert + File certFile = pipeStdoutToFile("cert.asc"); + pipeFileToStdin(keyFile); + assertSuccess(executeCommand("extract-cert")); + + // encrypt and sign message + String msg = "Hello, World!\n"; + pipeStringToStdin(msg); + File encryptedFile = pipeStdoutToFile("msg.asc"); + assertSuccess(executeCommand("encrypt", + "--sign-with", keyFile.getAbsolutePath(), + "--with-key-password", passwordFile.getAbsolutePath(), + "--no-armor", + "--as", "binary", + certFile.getAbsolutePath())); + + // Decrypt + File verificationsFile = nonExistentFile("verifications"); + pipeFileToStdin(encryptedFile); + ByteArrayOutputStream out = pipeStdoutToStream(); + assertSuccess(executeCommand("decrypt", + "--verify-with", certFile.getAbsolutePath(), + "--verifications-out", verificationsFile.getAbsolutePath(), + "--with-key-password", passwordFile.getAbsolutePath(), + keyFile.getAbsolutePath())); + + assertEquals(msg, out.toString()); + } + + @Test + public void decryptGarbageFails() throws IOException { + File keyFile = writeFile("key.asc", KEY); + pipeStringToStdin("Some Garbage!"); + int exitCode = executeCommand("decrypt", keyFile.getAbsolutePath()); + assertEquals(SOPGPException.BadData.EXIT_CODE, exitCode); + } + + @Test + public void decryptMessageWithWrongKeyFails() throws IOException { + File keyFile = pipeStdoutToFile("key.asc"); + assertSuccess(executeCommand("generate-key", "Bob ")); + // message was *not* created with key above + String ciphertext = "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "hF4D831k4umlLu4SAQdAYisjZTDRm217LHQbqjB766tm62CKTkRj3Gd0wYxVRCgw\n" + + "48SnOJINCJoPgDsxk2NiJmLCImoiET7IElqxN9htdDXQJwcRK+71r/ZyO4YJpWuX\n" + + "0sAAAcEFc3nT+un31sOi8KoBJlc5n+MemntQvcWDs8B87BEW/Ncjrs0s4pJpZKBQ\n" + + "/AWc4wLCI3ylfMQJB2pICqaOO3KP3WepgTIw5fuZm6YfriKQi7uZvVx1N+uaCIoa\n" + + "K2IVVf/7O9KZJ9GbsGYdpBj9IdaIZiVS3Xi8rwgQl3haI/EeHC3nnCsWyj23Fjt3\n" + + "LjbMqpHbSnp8U1cQ8rXavrREaKv69PFeJio6/hRg32TzJqn05dPALRxHMEkxxa4h\n" + + "FpVU\n" + + "=edS5\n" + + "-----END PGP MESSAGE-----"; + + pipeStringToStdin(ciphertext); + ByteArrayOutputStream out = pipeStdoutToStream(); + int exitCode = executeCommand("decrypt", keyFile.getAbsolutePath()); + assertEquals(SOPGPException.CannotDecrypt.EXIT_CODE, exitCode); + assertEquals(0, out.size()); + } + + @Test + public void encryptWithPasswordADecryptWithPasswordBFails() throws IOException { + File password1 = writeFile("password1", "swordfish"); + File password2 = writeFile("password2", "orange"); + + pipeStringToStdin("Bonjour, le monde!\n"); + ByteArrayOutputStream ciphertextOut = pipeStdoutToStream(); + assertSuccess(executeCommand("encrypt", "--with-password", password1.getAbsolutePath())); + + pipeBytesToStdin(ciphertextOut.toByteArray()); + ByteArrayOutputStream out = pipeStdoutToStream(); + int exitCode = executeCommand("decrypt", "--with-password", password2.getAbsolutePath()); + assertEquals(SOPGPException.CannotDecrypt.EXIT_CODE, exitCode); + assertEquals(0, out.size()); + } + + @Test + public void encryptWithGarbageCertFails() throws IOException { + File garbageCert = writeFile("cert.asc", "This is garbage!"); + + pipeStringToStdin("Hallo, Welt!\n"); + ByteArrayOutputStream out = pipeStdoutToStream(); + int exitCode = executeCommand("encrypt", garbageCert.getAbsolutePath()); + + assertEquals(SOPGPException.BadData.EXIT_CODE, exitCode); + assertEquals(0, out.size()); + } + + @Test + public void encrypt_signWithGarbageKeyFails() throws IOException { + File cert = writeFile("cert.asc", CERT); + File garbageKey = writeFile("key.asc", "This is not a key!"); + + pipeStringToStdin("Salut!\n"); + ByteArrayOutputStream out = pipeStdoutToStream(); + int exitCode = executeCommand("encrypt", "--sign-with", garbageKey.getAbsolutePath(), + cert.getAbsolutePath()); + + assertEquals(SOPGPException.BadData.EXIT_CODE, exitCode); + assertEquals(0, out.size()); + } + + @Test + public void decrypt_withGarbageKeyFails() throws IOException { + File key = writeFile("key.asc", "this is garbage"); + String ciphertext = "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "hF4D831k4umlLu4SAQdAYisjZTDRm217LHQbqjB766tm62CKTkRj3Gd0wYxVRCgw\n" + + "48SnOJINCJoPgDsxk2NiJmLCImoiET7IElqxN9htdDXQJwcRK+71r/ZyO4YJpWuX\n" + + "0sAAAcEFc3nT+un31sOi8KoBJlc5n+MemntQvcWDs8B87BEW/Ncjrs0s4pJpZKBQ\n" + + "/AWc4wLCI3ylfMQJB2pICqaOO3KP3WepgTIw5fuZm6YfriKQi7uZvVx1N+uaCIoa\n" + + "K2IVVf/7O9KZJ9GbsGYdpBj9IdaIZiVS3Xi8rwgQl3haI/EeHC3nnCsWyj23Fjt3\n" + + "LjbMqpHbSnp8U1cQ8rXavrREaKv69PFeJio6/hRg32TzJqn05dPALRxHMEkxxa4h\n" + + "FpVU\n" + + "=edS5\n" + + "-----END PGP MESSAGE-----"; + + pipeStringToStdin(ciphertext); + ByteArrayOutputStream out = pipeStdoutToStream(); + int exitCode = executeCommand("decrypt", key.getAbsolutePath()); + + assertEquals(SOPGPException.BadData.EXIT_CODE, exitCode); + assertEquals(0, out.size()); + } + + @Test + public void decrypt_verifyWithGarbageCertFails() throws IOException { + File key = writeFile("key.asc", KEY); + File cert = writeFile("cert.asc", "now this is garbage"); + String ciphertext = "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "hF4D831k4umlLu4SAQdAYisjZTDRm217LHQbqjB766tm62CKTkRj3Gd0wYxVRCgw\n" + + "48SnOJINCJoPgDsxk2NiJmLCImoiET7IElqxN9htdDXQJwcRK+71r/ZyO4YJpWuX\n" + + "0sAAAcEFc3nT+un31sOi8KoBJlc5n+MemntQvcWDs8B87BEW/Ncjrs0s4pJpZKBQ\n" + + "/AWc4wLCI3ylfMQJB2pICqaOO3KP3WepgTIw5fuZm6YfriKQi7uZvVx1N+uaCIoa\n" + + "K2IVVf/7O9KZJ9GbsGYdpBj9IdaIZiVS3Xi8rwgQl3haI/EeHC3nnCsWyj23Fjt3\n" + + "LjbMqpHbSnp8U1cQ8rXavrREaKv69PFeJio6/hRg32TzJqn05dPALRxHMEkxxa4h\n" + + "FpVU\n" + + "=edS5\n" + + "-----END PGP MESSAGE-----"; + File verificationsFile = nonExistentFile("verifications"); + + pipeStringToStdin(ciphertext); + ByteArrayOutputStream out = pipeStdoutToStream(); + int exitCode = executeCommand("decrypt", key.getAbsolutePath(), + "--verify-with", cert.getAbsolutePath(), + "--verifications-out", verificationsFile.getAbsolutePath()); + + assertEquals(SOPGPException.BadData.EXIT_CODE, exitCode); + assertEquals(0, out.size()); + } + + @Test + public void encryptWithProtectedKey_wrongPassphraseFails() throws IOException { + File password = writeFile("passphrase1", "orange"); + File wrongPassword = writeFile("passphrase2", "blue"); + + File keyFile = pipeStdoutToFile("key.asc"); + assertSuccess(executeCommand("generate-key", "Pedro ", + "--with-key-password", password.getAbsolutePath())); + + File certFile = pipeStdoutToFile("cert.asc"); + pipeFileToStdin(keyFile); + assertSuccess(executeCommand("extract-cert")); + + // Use no passphrase to unlock the key + String msg = "Guten Tag, Welt!\n"; + pipeStringToStdin(msg); + ByteArrayOutputStream out = pipeStdoutToStream(); + int exitCode = executeCommand("encrypt", "--sign-with", keyFile.getAbsolutePath(), + certFile.getAbsolutePath()); + assertEquals(SOPGPException.KeyIsProtected.EXIT_CODE, exitCode); + assertEquals(0, out.size()); + + // use wrong passphrase to unlock key when signing message + pipeStringToStdin("Guten Tag, Welt!\n"); + out = pipeStdoutToStream(); + exitCode = executeCommand("encrypt", "--sign-with", keyFile.getAbsolutePath(), + "--with-key-password", wrongPassword.getAbsolutePath(), + certFile.getAbsolutePath()); + assertEquals(SOPGPException.KeyIsProtected.EXIT_CODE, exitCode); + assertEquals(0, out.size()); + + // use correct passphrase and encrypt+sign message + pipeStringToStdin("Guten Tag, Welt!\n"); + out = pipeStdoutToStream(); + assertSuccess(executeCommand("encrypt", "--sign-with", keyFile.getAbsolutePath(), + "--with-key-password", password.getAbsolutePath(), + certFile.getAbsolutePath())); + String ciphertext = out.toString(); + + // Use no passphrase to decrypt key when decrypting + pipeStringToStdin(ciphertext); + out = pipeStdoutToStream(); + exitCode = executeCommand("decrypt", keyFile.getAbsolutePath()); + assertEquals(SOPGPException.KeyIsProtected.EXIT_CODE, exitCode); + assertEquals(0, out.size()); + + // Use wrong passphrase to decrypt key when decrypting + pipeStringToStdin(ciphertext); + out = pipeStdoutToStream(); + exitCode = executeCommand("decrypt", "--with-key-password", wrongPassword.getAbsolutePath(), + keyFile.getAbsolutePath()); + assertEquals(SOPGPException.KeyIsProtected.EXIT_CODE, exitCode); + assertEquals(0, out.size()); + + // User correct passphrase to decrypt key when decrypting + pipeStringToStdin(ciphertext); + out = pipeStdoutToStream(); + assertSuccess(executeCommand("decrypt", "--with-key-password", password.getAbsolutePath(), + keyFile.getAbsolutePath())); + assertEquals(msg, out.toString()); + } + + @Test + public void decryptMalformedMessageYieldsBadData() throws IOException { + // Message contains encrypted data packet which contains the plaintext directly - no literal data packet. + // It is therefore malformed. + String malformed = "-----BEGIN PGP MESSAGE-----\n" + + "Version: BCPG v1.72b04\n" + + "\n" + + "hF4D831k4umlLu4SAQdApKA6VDKSLQvwS2kbWqlhcXD8XHdFkSccqv5tBptZnBgw\n" + + "nZNXVhwUpap0ymb4jPTD+EVPKOfPyy04ouIGZAJKkfYDeSL/8sKcbnPPuQJYYjGQ\n" + + "ySDNmidrtTonwcSuwAfRyn74BBqOVhrr8GXkVIfevIlZFQ==\n" + + "=wIgl\n" + + "-----END PGP MESSAGE-----"; + File key = writeFile("key.asc", KEY); + pipeStringToStdin(malformed); + int exitCode = executeCommand("decrypt", key.getAbsolutePath()); + assertEquals(SOPGPException.BadData.EXIT_CODE, exitCode); + } + + @Test + public void decryptWithPasswordWithPendingWhitespaceWorks() throws IOException { + assertEncryptWithPasswordADecryptWithPasswordBWorks("sw0rdf1sh", "sw0rdf1sh \n"); + } + + @Test + public void encryptWithTrailingWhitespaceDecryptWithoutWorks() throws IOException { + assertEncryptWithPasswordADecryptWithPasswordBWorks("sw0rdf1sh \n", "sw0rdf1sh"); + } + + @Test + public void decryptWithWhitespacePasswordWorks() throws IOException { + // is encrypted for "sw0rdf1sh \n" + String encryptedToPasswordWithTrailingWhitespace = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "jA0ECQMC32tEJug0BCpg0kABfT3dKgA4K8XGpk2ul67BXLZD//fCCSmIQIWnNhE1\n" + + "6q97xFQ628K8f/58+XoBzLqLDT+LEz9Bz+Yg9QfzkEFy\n" + + "=2Y+K\n" + + "-----END PGP MESSAGE-----"; + pipeStringToStdin(encryptedToPasswordWithTrailingWhitespace); + File password = writeFile("password", "sw0rdf1sh \n"); + ByteArrayOutputStream plaintext = pipeStdoutToStream(); + assertSuccess(executeCommand("decrypt", "--with-password", password.getAbsolutePath())); + + assertEquals("Hello, World!\n", plaintext.toString()); + } + + private void assertEncryptWithPasswordADecryptWithPasswordBWorks(String passwordA, String passwordB) + throws IOException { + File passwordAFile = writeFile("password", passwordA); + File passwordBFile = writeFile("passwordWithWS", passwordB); + + String msg = "Hello, World!\n"; + pipeStringToStdin(msg); + ByteArrayOutputStream ciphertext = pipeStdoutToStream(); + assertSuccess(executeCommand("encrypt", "--with-password", passwordAFile.getAbsolutePath())); + + pipeStringToStdin(ciphertext.toString()); + ByteArrayOutputStream plaintext = pipeStdoutToStream(); + assertSuccess(executeCommand("decrypt", "--with-password", passwordBFile.getAbsolutePath())); + + assertEquals(msg, plaintext.toString()); + } + + @Test + public void testDecryptWithoutDecryptionOptionFails() throws IOException { + String ciphertext = "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "hF4D831k4umlLu4SAQdAYisjZTDRm217LHQbqjB766tm62CKTkRj3Gd0wYxVRCgw\n" + + "48SnOJINCJoPgDsxk2NiJmLCImoiET7IElqxN9htdDXQJwcRK+71r/ZyO4YJpWuX\n" + + "0sAAAcEFc3nT+un31sOi8KoBJlc5n+MemntQvcWDs8B87BEW/Ncjrs0s4pJpZKBQ\n" + + "/AWc4wLCI3ylfMQJB2pICqaOO3KP3WepgTIw5fuZm6YfriKQi7uZvVx1N+uaCIoa\n" + + "K2IVVf/7O9KZJ9GbsGYdpBj9IdaIZiVS3Xi8rwgQl3haI/EeHC3nnCsWyj23Fjt3\n" + + "LjbMqpHbSnp8U1cQ8rXavrREaKv69PFeJio6/hRg32TzJqn05dPALRxHMEkxxa4h\n" + + "FpVU\n" + + "=edS5\n" + + "-----END PGP MESSAGE-----"; + pipeStringToStdin(ciphertext); + int exitCode = executeCommand("decrypt"); + assertEquals(SOPGPException.MissingArg.EXIT_CODE, exitCode); + } + + @Test + public void testEncryptDecryptWithFreshRSAKey() throws IOException { + // Generate key + File passwordFile = writeFile("password", "sw0rdf1sh"); + File keyFile = pipeStdoutToFile("key.asc"); + assertSuccess(executeCommand("generate-key", "--profile=rfc4880", "--with-key-password", passwordFile.getAbsolutePath(), "Alice ")); + + File certFile = pipeStdoutToFile("cert.asc"); + pipeFileToStdin(keyFile); + assertSuccess(executeCommand("extract-cert")); + + // Write plaintext + File plaintextFile = writeFile("msg.txt", "Hello, World!\n"); + + // Encrypt + File ciphertextFile = pipeStdoutToFile("msg.asc"); + pipeFileToStdin(plaintextFile); + assertSuccess(executeCommand("encrypt", "--profile=rfc4880", certFile.getAbsolutePath())); + + ByteArrayOutputStream decrypted = pipeStdoutToStream(); + pipeFileToStdin(ciphertextFile); + assertSuccess(executeCommand("decrypt", "--with-key-password", passwordFile.getAbsolutePath(), keyFile.getAbsolutePath())); + + assertEquals("Hello, World!\n", decrypted.toString()); + } +} diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java new file mode 100644 index 00000000..057cec98 --- /dev/null +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java @@ -0,0 +1,466 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.cli.commands; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import org.bouncycastle.bcpg.ArmoredOutputStream; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.CompressionAlgorithm; +import org.pgpainless.encryption_signing.EncryptionStream; +import org.pgpainless.encryption_signing.ProducerOptions; +import org.pgpainless.encryption_signing.SigningOptions; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.slf4j.LoggerFactory; +import sop.exception.SOPGPException; + +public class RoundTripInlineSignInlineVerifyCmdTest extends CLITest { + + public RoundTripInlineSignInlineVerifyCmdTest() { + super(LoggerFactory.getLogger(RoundTripInlineSignInlineVerifyCmdTest.class)); + } + + private static final String KEY_1_PASSWORD = "takeDemHobbits2Isengard"; + private static final String KEY_1 = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 59F4 EC7D 4A87 3E69 7029 8FDE 9FF0 8738 DFC0 0224\n" + + "Comment: Legolas \n" + + "\n" + + "lIYEY2wKdxYJKwYBBAHaRw8BAQdALfUbOSOsPDg4IgX7Mrub3EtkX0rp02orL/0j\n" + + "2VpV1rf+CQMCVICwUO0SkvdgcPdvXO1cW4KIp6HCVVV6VgU5cvBlmrk9PNUQVBkb\n" + + "6S7oXQu0CgGwJ+QdbooBQqOjMy2MDy+UXaURTaVyWcmetsZJZzD2wrQhTGVnb2xh\n" + + "cyA8bGVnb2xhc0BmZWxsb3dzaGlwLnJpbmc+iI8EExYKAEEFAmNsCncJEJ/whzjf\n" + + "wAIkFiEEWfTsfUqHPmlwKY/en/CHON/AAiQCngECmwEFFgIDAQAECwkIBwUVCgkI\n" + + "CwKZAQAAE10BAN9tN4Le1p4giS6P/yFuKFlDBOeiq1S4EqwYG7qdcqemAP45O3w4\n" + + "3sXliOJBGDR/l/lOMHdPcTOb7VRwWbpIqx8LBJyLBGNsCncSCisGAQQBl1UBBQEB\n" + + "B0AMc+7s6uBqAQcDvfKkD5zYbmB9ZfwIjRWQq/XF+g8KQwMBCAf+CQMCVICwUO0S\n" + + "kvdgHLmKhKW1xxCNZAqQcIHa9F/cqb6Sq/oVFHj2bEYzmGVvFCVUpP7KJWGTeFT+\n" + + "BYK779quIqjxHOfzC3Jmo3BHkUPWYOa0rIh1BBgWCgAdBQJjbAp3Ap4BApsMBRYC\n" + + "AwEABAsJCAcFFQoJCAsACgkQn/CHON/AAiRUewD9HtKrCUf3S1yR28emzITWPgJS\n" + + "UA5mkzEMnYspV7zU4jgA/R6jj/5QqPszElCQNZGtvsDUwYo10iRlQkxPshcPNakJ\n" + + "nIYEY2wKdxYJKwYBBAHaRw8BAQdAYxpRGib/f/tu65gbsV22nmysVVmVgiQuDxyH\n" + + "rz7VCi/+CQMCVICwUO0SkvdgOYYbWltjQRDM3SW/Zw/DiZN9MYZYa0MTgs0SHoaM\n" + + "5LU7jMxNmPR1UtSqEO36QqW91q4fpEkGrdWE4gwjm1bth8pyYKiSFojVBBgWCgB9\n" + + "BQJjbAp3Ap4BApsCBRYCAwEABAsJCAcFFQoJCAtfIAQZFgoABgUCY2wKdwAKCRCW\n" + + "K491s9xIMHwKAQDpSWQqiFxFvls9eRGtJ1eQT+L3Z2rDel5zNV44IdTf/wEA0vnJ\n" + + "ouSKKuiH6Ck2OEkXbElH6gdQvOCYA7Z9gVeeHQoACgkQn/CHON/AAiSD6QD+LTZx\n" + + "NU+t4wQlWOkSsjOLsH/Sk5DZq+4HyQnStlxUJpUBALZFkZps65IP03VkPnQWigfs\n" + + "YgztJA1z/rmm3fmFgMMG\n" + + "=daDH\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + private static final String CERT_1 = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 59F4 EC7D 4A87 3E69 7029 8FDE 9FF0 8738 DFC0 0224\n" + + "Comment: Legolas \n" + + "\n" + + "mDMEY2wKdxYJKwYBBAHaRw8BAQdALfUbOSOsPDg4IgX7Mrub3EtkX0rp02orL/0j\n" + + "2VpV1re0IUxlZ29sYXMgPGxlZ29sYXNAZmVsbG93c2hpcC5yaW5nPoiPBBMWCgBB\n" + + "BQJjbAp3CRCf8Ic438ACJBYhBFn07H1Khz5pcCmP3p/whzjfwAIkAp4BApsBBRYC\n" + + "AwEABAsJCAcFFQoJCAsCmQEAABNdAQDfbTeC3taeIIkuj/8hbihZQwTnoqtUuBKs\n" + + "GBu6nXKnpgD+OTt8ON7F5YjiQRg0f5f5TjB3T3Ezm+1UcFm6SKsfCwS4OARjbAp3\n" + + "EgorBgEEAZdVAQUBAQdADHPu7OrgagEHA73ypA+c2G5gfWX8CI0VkKv1xfoPCkMD\n" + + "AQgHiHUEGBYKAB0FAmNsCncCngECmwwFFgIDAQAECwkIBwUVCgkICwAKCRCf8Ic4\n" + + "38ACJFR7AP0e0qsJR/dLXJHbx6bMhNY+AlJQDmaTMQydiylXvNTiOAD9HqOP/lCo\n" + + "+zMSUJA1ka2+wNTBijXSJGVCTE+yFw81qQm4MwRjbAp3FgkrBgEEAdpHDwEBB0Bj\n" + + "GlEaJv9/+27rmBuxXbaebKxVWZWCJC4PHIevPtUKL4jVBBgWCgB9BQJjbAp3Ap4B\n" + + "ApsCBRYCAwEABAsJCAcFFQoJCAtfIAQZFgoABgUCY2wKdwAKCRCWK491s9xIMHwK\n" + + "AQDpSWQqiFxFvls9eRGtJ1eQT+L3Z2rDel5zNV44IdTf/wEA0vnJouSKKuiH6Ck2\n" + + "OEkXbElH6gdQvOCYA7Z9gVeeHQoACgkQn/CHON/AAiSD6QD+LTZxNU+t4wQlWOkS\n" + + "sjOLsH/Sk5DZq+4HyQnStlxUJpUBALZFkZps65IP03VkPnQWigfsYgztJA1z/rmm\n" + + "3fmFgMMG\n" + + "=/lYl\n" + + "-----END PGP PUBLIC KEY BLOCK-----"; + private static final String CERT_1_SIGNING_KEY = + "D8906FEB9842569834FEDA9E962B8F75B3DC4830 59F4EC7D4A873E6970298FDE9FF08738DFC00224"; + + private static final String KEY_2 = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: AEA0 FD2C 899D 3FC0 7781 5F00 2656 0D2A E53D B86F\n" + + "Comment: Gollum \n" + + "\n" + + "lFgEY2wKphYJKwYBBAHaRw8BAQdA9MXACulaJvjIuMKbsc+/fLJ523lODbHmuTpc\n" + + "jpPdjaEAAP9Edg7yeIGEeNP0GrndUpNeZyFAXAlCHJObDbS80G6BBw9ktBlHb2xs\n" + + "dW0gPGdvbGx1bUBkZWVwLmNhdmU+iI8EExYKAEEFAmNsCqYJECZWDSrlPbhvFiEE\n" + + "rqD9LImdP8B3gV8AJlYNKuU9uG8CngECmwEFFgIDAQAECwkIBwUVCgkICwKZAQAA\n" + + "KSkBAOMq6ymNH83E5CBA/mn3DYLhnujzC9cVf/iX2zrsdXMvAQCWdfFy/PlGhP3K\n" + + "M+ej6WIRsx24Yy/NhNPcRJUzcv6dC5xdBGNsCqYSCisGAQQBl1UBBQEBB0DiN/5n\n" + + "AFQafWjnSkKhctFCNkfVRrnAea/2T/D8fYWeYwMBCAcAAP9HbxOhwxqz8I+pwk3e\n" + + "kZXNolWqagrYZkpNvqlBb/JJWBGViHUEGBYKAB0FAmNsCqYCngECmwwFFgIDAQAE\n" + + "CwkIBwUVCgkICwAKCRAmVg0q5T24bw2EAP4pUHVA2pkVspzEttIaQxdoHcnbwjae\n" + + "q12TmWqWDFFvwgD+O2EqHn0iXW49EOQrlP8g+bdWUlT0ZIW3C3Fv7nNA3AScWARj\n" + + "bAqmFgkrBgEEAdpHDwEBB0BHsmdF1Q0aU3YRVDeXGb904Nb7H/cxcasDhcbu2FTo\n" + + "HAAA/j1+WzozN/3lefo76eyENKkXl4f1rQlUreqytuaTsb0WEq6I1QQYFgoAfQUC\n" + + "Y2wKpgKeAQKbAgUWAgMBAAQLCQgHBRUKCQgLXyAEGRYKAAYFAmNsCqYACgkQj73T\n" + + "bQGDFnN9OwD/QDDi1qq7DrGlENQf2mPDh36YgM7bREY1vHEbbUNoqy4A/RJzMuwt\n" + + "L1M49UzQS7OIGP12/9cT66XPGjpCL+6zLPwCAAoJECZWDSrlPbhvw3ABAOE7/Iit\n" + + "ntMexrSK5jCd9JdCCNb2rjR6XA18rXFGOrVBAPwLKAogNFQlP2kUsObTnIaTCro2\n" + + "cjK8WE1pfIwQ0ArPCQ==\n" + + "=SzrG\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + private static final String CERT_2 = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: AEA0 FD2C 899D 3FC0 7781 5F00 2656 0D2A E53D B86F\n" + + "Comment: Gollum \n" + + "\n" + + "mDMEY2wKphYJKwYBBAHaRw8BAQdA9MXACulaJvjIuMKbsc+/fLJ523lODbHmuTpc\n" + + "jpPdjaG0GUdvbGx1bSA8Z29sbHVtQGRlZXAuY2F2ZT6IjwQTFgoAQQUCY2wKpgkQ\n" + + "JlYNKuU9uG8WIQSuoP0siZ0/wHeBXwAmVg0q5T24bwKeAQKbAQUWAgMBAAQLCQgH\n" + + "BRUKCQgLApkBAAApKQEA4yrrKY0fzcTkIED+afcNguGe6PML1xV/+JfbOux1cy8B\n" + + "AJZ18XL8+UaE/coz56PpYhGzHbhjL82E09xElTNy/p0LuDgEY2wKphIKKwYBBAGX\n" + + "VQEFAQEHQOI3/mcAVBp9aOdKQqFy0UI2R9VGucB5r/ZP8Px9hZ5jAwEIB4h1BBgW\n" + + "CgAdBQJjbAqmAp4BApsMBRYCAwEABAsJCAcFFQoJCAsACgkQJlYNKuU9uG8NhAD+\n" + + "KVB1QNqZFbKcxLbSGkMXaB3J28I2nqtdk5lqlgxRb8IA/jthKh59Il1uPRDkK5T/\n" + + "IPm3VlJU9GSFtwtxb+5zQNwEuDMEY2wKphYJKwYBBAHaRw8BAQdAR7JnRdUNGlN2\n" + + "EVQ3lxm/dODW+x/3MXGrA4XG7thU6ByI1QQYFgoAfQUCY2wKpgKeAQKbAgUWAgMB\n" + + "AAQLCQgHBRUKCQgLXyAEGRYKAAYFAmNsCqYACgkQj73TbQGDFnN9OwD/QDDi1qq7\n" + + "DrGlENQf2mPDh36YgM7bREY1vHEbbUNoqy4A/RJzMuwtL1M49UzQS7OIGP12/9cT\n" + + "66XPGjpCL+6zLPwCAAoJECZWDSrlPbhvw3ABAOE7/IitntMexrSK5jCd9JdCCNb2\n" + + "rjR6XA18rXFGOrVBAPwLKAogNFQlP2kUsObTnIaTCro2cjK8WE1pfIwQ0ArPCQ==\n" + + "=j1LR\n" + + "-----END PGP PUBLIC KEY BLOCK-----"; + private static final String CERT_2_SIGNING_KEY = + "7A073EDF273C902796D259528FBDD36D01831673 AEA0FD2C899D3FC077815F0026560D2AE53DB86F"; + + private static final String MESSAGE = "One does not simply use OpenPGP!\n" + + "\n" + + "There is only one Lord of the Keys, only one who can bend them to his will. And he does not share power."; + + private static final String MESSAGE_CRLF = "One does not simply use OpenPGP!\r\n" + + "\r\n" + + "There is only one Lord of the Keys, only one who can bend them to his will. And he does not share power."; + + @Test + public void createCleartextSignedMessage() throws IOException { + File key = writeFile("key.asc", KEY_1); + File password = writeFile("password", KEY_1_PASSWORD); + + pipeStringToStdin(MESSAGE); + ByteArrayOutputStream ciphertextOut = pipeStdoutToStream(); + assertSuccess(executeCommand("inline-sign", + "--as", "clearsigned", + key.getAbsolutePath(), + "--with-key-password", password.getAbsolutePath())); + + String cleartextSigned = ciphertextOut.toString(); + assertTrue(cleartextSigned.startsWith("-----BEGIN PGP SIGNED MESSAGE-----\n" + + "Hash: ")); + assertTrue(cleartextSigned.contains(MESSAGE_CRLF)); + assertTrue(cleartextSigned.contains("\n-----BEGIN PGP SIGNATURE-----\n")); + assertTrue(cleartextSigned.endsWith("-----END PGP SIGNATURE-----\n")); + } + + @Test + public void createAndVerifyCleartextSignedMessage() throws IOException { + File key = writeFile("key.asc", KEY_1); + File password = writeFile("password", KEY_1_PASSWORD); + + pipeStringToStdin(MESSAGE); + ByteArrayOutputStream ciphertextOut = pipeStdoutToStream(); + assertSuccess(executeCommand("inline-sign", + "--as", "clearsigned", + key.getAbsolutePath(), + "--with-key-password", password.getAbsolutePath())); + + File cert = writeFile("cert.asc", CERT_1); + File verifications = nonExistentFile("verifications"); + pipeStringToStdin(ciphertextOut.toString()); + ByteArrayOutputStream plaintextOut = pipeStdoutToStream(); + assertSuccess(executeCommand("inline-verify", + "--verifications-out", verifications.getAbsolutePath(), + cert.getAbsolutePath())); + + assertEquals(MESSAGE, plaintextOut.toString()); + String verificationString = readStringFromFile(verifications); + assertTrue(verificationString.contains(CERT_1_SIGNING_KEY)); + } + + @Test + public void createAndVerifyTextSignedMessage() throws IOException { + File key = writeFile("key.asc", KEY_1); + File password = writeFile("password", KEY_1_PASSWORD); + + pipeStringToStdin(MESSAGE); + ByteArrayOutputStream ciphertextOut = pipeStdoutToStream(); + assertSuccess(executeCommand("inline-sign", + "--as", "text", + key.getAbsolutePath(), + "--with-key-password", password.getAbsolutePath())); + + File cert = writeFile("cert.asc", CERT_1); + File verifications = nonExistentFile("verifications"); + pipeStringToStdin(ciphertextOut.toString()); + ByteArrayOutputStream plaintextOut = pipeStdoutToStream(); + assertSuccess(executeCommand("inline-verify", + "--verifications-out", verifications.getAbsolutePath(), + cert.getAbsolutePath())); + + assertEquals(MESSAGE_CRLF, plaintextOut.toString()); + String verificationString = readStringFromFile(verifications); + assertTrue(verificationString.contains(CERT_1_SIGNING_KEY)); + } + + @Test + public void createSignedMessageWithKeyAAndVerifyWithKeyBFails() throws IOException { + File key = writeFile("key.asc", KEY_1); + File password = writeFile("password", KEY_1_PASSWORD); + File cert = writeFile("cert.asc", CERT_2); // mismatch + + pipeStringToStdin(MESSAGE); + ByteArrayOutputStream ciphertextOut = pipeStdoutToStream(); + assertSuccess(executeCommand("inline-sign", + key.getAbsolutePath(), + "--with-key-password", password.getAbsolutePath())); + + File verifications = nonExistentFile("verifications"); + pipeStringToStdin(ciphertextOut.toString()); + ByteArrayOutputStream plaintextOut = pipeStdoutToStream(); + int exitCode = executeCommand("inline-verify", + "--verifications-out", verifications.getAbsolutePath(), + cert.getAbsolutePath()); + + assertEquals(SOPGPException.NoSignature.EXIT_CODE, exitCode); + assertEquals(MESSAGE, plaintextOut.toString()); // message is emitted nonetheless + assertFalse(verifications.exists(), "Verifications file MUST NOT be written."); + } + + @Test + public void createAndVerifyMultiKeyBinarySignedMessage() throws IOException { + File key1Pass = writeFile("password", KEY_1_PASSWORD); + File key1 = writeFile("key1.asc", KEY_1); + File key2 = writeFile("key2.asc", KEY_2); + + pipeStringToStdin(MESSAGE); + ByteArrayOutputStream ciphertextOut = pipeStdoutToStream(); + assertSuccess(executeCommand("inline-sign", + "--as", "binary", + "--no-armor", + key2.getAbsolutePath(), + "--with-key-password", key1Pass.getAbsolutePath(), + key1.getAbsolutePath())); + + assertFalse(ciphertextOut.toString().startsWith("-----BEGIN PGP SIGNED MESSAGE-----\n")); + byte[] unarmoredMessage = ciphertextOut.toByteArray(); + + File cert1 = writeFile("cert1.asc", CERT_1); + File cert2 = writeFile("cert2.asc", CERT_2); + File verificationFile = nonExistentFile("verifications"); + pipeBytesToStdin(unarmoredMessage); + ByteArrayOutputStream plaintextOut = pipeStdoutToStream(); + assertSuccess(executeCommand("inline-verify", + "--verifications-out", verificationFile.getAbsolutePath(), + cert1.getAbsolutePath(), cert2.getAbsolutePath())); + + assertEquals(MESSAGE, plaintextOut.toString()); + String verification = readStringFromFile(verificationFile); + assertTrue(verification.contains(CERT_1_SIGNING_KEY)); + assertTrue(verification.contains(CERT_2_SIGNING_KEY)); + } + + @Test + public void createTextSignedMessageInlineDetachAndDetachedVerify() throws IOException { + File key = writeFile("key.asc", KEY_1); + File password = writeFile("password", KEY_1_PASSWORD); + + pipeStringToStdin(MESSAGE); + ByteArrayOutputStream ciphertextOut = pipeStdoutToStream(); + assertSuccess(executeCommand("inline-sign", + "--as", "clearsigned", + key.getAbsolutePath(), + "--with-key-password", password.getAbsolutePath())); + + File sigFile = nonExistentFile("sig.asc"); + pipeStringToStdin(ciphertextOut.toString()); + ByteArrayOutputStream msgOut = pipeStdoutToStream(); + assertSuccess(executeCommand("inline-detach", + "--signatures-out", sigFile.getAbsolutePath())); + assertEquals(MESSAGE, msgOut.toString()); + + File cert = writeFile("cert.asc", CERT_1); + pipeStringToStdin(msgOut.toString()); + ByteArrayOutputStream verificationsOut = pipeStdoutToStream(); + assertSuccess(executeCommand("verify", "--stacktrace", + sigFile.getAbsolutePath(), + cert.getAbsolutePath())); + + String verificationString = verificationsOut.toString(); + assertTrue(verificationString.contains(CERT_1_SIGNING_KEY)); + } + + @Test + public void testUnlockKeyWithOneOfMultiplePasswords() throws IOException { + File key = writeFile("key.asc", KEY_1); + File wrong1 = writeFile("wrong_1", "BuzzAldr1n"); + File correct = writeFile("correct", KEY_1_PASSWORD); + File wrong2 = writeFile("wrong_2", "NeilArmstr0ng"); + + pipeStringToStdin(MESSAGE); + ByteArrayOutputStream ciphertextOut = pipeStdoutToStream(); + assertSuccess(executeCommand("inline-sign", + key.getAbsolutePath(), + "--with-key-password", wrong1.getAbsolutePath(), + "--with-key-password", correct.getAbsolutePath(), + "--with-key-password", wrong2.getAbsolutePath())); + + File cert = writeFile("cert.asc", CERT_1); + pipeStringToStdin(ciphertextOut.toString()); + ByteArrayOutputStream msgOut = pipeStdoutToStream(); + File verificationsFile = nonExistentFile("verifications"); + assertSuccess(executeCommand("inline-verify", + "--verifications-out", verificationsFile.getAbsolutePath(), + cert.getAbsolutePath())); + + assertEquals(MESSAGE, msgOut.toString()); + String verificationString = readStringFromFile(verificationsFile); + assertTrue(verificationString.contains(CERT_1_SIGNING_KEY)); + } + + @Test + public void cannotVerifyEncryptedMessage() throws IOException { + File key = writeFile("key.asc", KEY_2); + File cert = writeFile("cert.asc", CERT_2); + + String msg = "Hello, World!\n"; + pipeStringToStdin(msg); + ByteArrayOutputStream ciphertext = pipeStdoutToStream(); + assertSuccess(executeCommand("encrypt", cert.getAbsolutePath(), + "--sign-with", key.getAbsolutePath())); + + File verifications = nonExistentFile("verifications"); + pipeBytesToStdin(ciphertext.toByteArray()); + ByteArrayOutputStream out = pipeStdoutToStream(); + int exitCode = executeCommand("inline-verify", cert.getAbsolutePath(), + "--verifications-out", verifications.getAbsolutePath()); + + assertEquals(SOPGPException.BadData.EXIT_CODE, exitCode); + assertEquals(0, out.size()); + } + + @Test + public void createMalformedMessage() throws IOException, PGPException { + String msg = "Hello, World!\n"; + PGPSecretKeyRing key = PGPainless.readKeyRing().secretKeyRing(KEY_2); + ByteArrayOutputStream ciphertext = new ByteArrayOutputStream(); + EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() + .onOutputStream(ciphertext) + .withOptions(ProducerOptions.sign(SigningOptions.get() + .addDetachedSignature(SecretKeyRingProtector.unprotectedKeys(), key) + ).overrideCompressionAlgorithm(CompressionAlgorithm.UNCOMPRESSED) + .setAsciiArmor(false)); + encryptionStream.write(msg.getBytes(StandardCharsets.UTF_8)); + encryptionStream.close(); + PGPSignature sig = encryptionStream.getResult().getDetachedSignatures().entrySet() + .iterator().next().getValue().iterator().next(); + ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out); + armorOut.write(ciphertext.toByteArray()); + armorOut.write(sig.getEncoded()); + armorOut.close(); + } + + @Test + public void cannotVerifyMalformedMessage() throws IOException { + // appended signature -> malformed + String malformedSignedMessage = "-----BEGIN PGP MESSAGE-----\n" + + "Version: BCPG v1.72b04\n" + + "\n" + + "yxRiAAAAAABIZWxsbywgV29ybGQhCoh1BAAWCgAnBQJjd52aCRCPvdNtAYMWcxYh\n" + + "BHoHPt8nPJAnltJZUo+9020BgxZzAACThwD/Vr7CMitMOul40VK12XXjOv5f8vgi\n" + + "ksqhrI2ysItID9oA/0Csgf3Sv2YenYVzqnd0hhiPe5IVPl8w4sTZKpriYMIG\n" + + "=DPPU\n" + + "-----END PGP MESSAGE-----"; + File cert = writeFile("cert.asc", CERT_2); + File verifications = nonExistentFile("verifications"); + + pipeStringToStdin(malformedSignedMessage); + ByteArrayOutputStream out = pipeStdoutToStream(); + int exitCode = executeCommand("inline-verify", cert.getAbsolutePath(), + "--verifications-out", verifications.getAbsolutePath()); + + assertEquals(SOPGPException.BadData.EXIT_CODE, exitCode); + assertEquals("Hello, World!\n", out.toString()); + } + + @Test + public void verifyPrependedSignedMessage() throws IOException { + // message with prepended signature + String malformedSignedMessage = "-----BEGIN PGP SIGNATURE-----\n" + + "Version: BCPG v1.72b04\n" + + "\n" + + "iHUEABYKACcFAmN3nOUJEI+9020BgxZzFiEEegc+3yc8kCeW0llSj73TbQGDFnMA\n" + + "ANPKAPkBxLVHvgeCkX/tTHdBH3CDeuUQF2wmtUmGXqhZA1IFtwD/dK0XQBHO3RO+\n" + + "GHpzA7fDAroqF0zM72tu2W4PPw04FgKjATstksQAAh6pOTn5Ogrh+UU5KYpcAA==\n" + + "=xtik\n" + + "-----END PGP SIGNATURE-----"; + File cert = writeFile("cert.asc", CERT_2); + File verifications = nonExistentFile("verifications"); + + pipeStringToStdin(malformedSignedMessage); + ByteArrayOutputStream out = pipeStdoutToStream(); + assertSuccess(executeCommand("inline-verify", cert.getAbsolutePath(), + "--verifications-out", verifications.getAbsolutePath())); + assertEquals("Hello, World!\n", out.toString()); + String ver = readStringFromFile(verifications); + assertEquals( + "2022-11-18T14:55:33Z 7A073EDF273C902796D259528FBDD36D01831673 AEA0FD2C899D3FC077815F0026560D2AE53DB86F mode:binary\n", ver); + } + + @Test + public void testInlineSignWithMissingSecretKeysFails() throws IOException { + String missingSecretKeys = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Comment: 8677 37CA 1979 28FA 325A DE56 B455 9329 9882 36BE\n" + + "Comment: Mrs. Secret Key \n" + + "\n" + + "lEwEY3t3pRYJKwYBBAHaRw8BAQdA7lifUc85s7omw7eYNIaIj2mZrGeZ9KkG0WX2\n" + + "hAx5qXT+AGUAR05VAhAAAAAAAAAAAAAAAAAAAAAAtCFNcnMuIFNlY3JldCBLZXkg\n" + + "PG1pc3NAc2VjcmV0LmtleT6IjwQTFgoAQQUCY3t3pQkQtFWTKZiCNr4WIQSGdzfK\n" + + "GXko+jJa3la0VZMpmII2vgKeAQKbAQUWAgMBAAQLCQgHBRUKCQgLApkBAABNTQEA\n" + + "uU5L9hJ1QKWxL5wetJwR08rXJTzsuX1LRfy8dlnlJl0BAKPSqydLoTEVlJQ/2sjO\n" + + "xQmc6aedoOoXKKVNDW5ibrsEnFEEY3t3pRIKKwYBBAGXVQEFAQEHQA/WdwR+NFaY\n" + + "7NeZnRwI3X9sI5fMq0vtEauMLfZjqTc/AwEIB/4AZQBHTlUCEAAAAAAAAAAAAAAA\n" + + "AAAAAACIdQQYFgoAHQUCY3t3pQKeAQKbDAUWAgMBAAQLCQgHBRUKCQgLAAoJELRV\n" + + "kymYgja+8XMA/1quBVvaSf4QxbB2S7rKt93rAynDLqGQD8hC6wiZc+ihAQC87n2r\n" + + "meZ9kiYLYiQuBTGvXyzDBtw5m7wQtMWTfXisBpxMBGN7d6UWCSsGAQQB2kcPAQEH\n" + + "QMguDhFon0ZI//CIpC2ZndmtvKdJhcEAeVNkdcsIZajl/gBlAEdOVQIQAAAAAAAA\n" + + "AAAAAAAAAAAAAIjVBBgWCgB9BQJje3elAp4BApsCBRYCAwEABAsJCAcFFQoJCAtf\n" + + "IAQZFgoABgUCY3t3pQAKCRC14KclsvqqOstPAQDYiL7+4HucWKmd7dcd9XJZpdB6\n" + + "lneoK0qku0wvTVjX7gEAtUt2eXMlBE4ox+ZmY964PCc2gEHuC7PBtsAzuF7GSQwA\n" + + "CgkQtFWTKZiCNr7JKwEA3aLsOWAYzqvKgiboYSzle+SVBUb3chKlzf3YmckjmwgA\n" + + "/3YN1W8CiQFvE9NvetZkr2wXB+QVkuL6cxM0ogEo4lAG\n" + + "=9ZMl\n" + + "-----END PGP PRIVATE KEY BLOCK-----\n"; + File key = writeFile("key.asc", missingSecretKeys); + + pipeStringToStdin("Hello, World!\n"); + ByteArrayOutputStream out = pipeStdoutToStream(); + int exitCode = executeCommand("inline-sign", key.getAbsolutePath()); + + assertEquals(SOPGPException.KeyCannotSign.EXIT_CODE, exitCode); + assertEquals(0, out.size()); + } + + @Test + public void signWithProtectedKeyWithWrongPassphraseFails() throws IOException { + File key = writeFile("key.asc", KEY_1); + File password = writeFile("password.asc", "not_correct!"); + + pipeStringToStdin("Hello, World!\n"); + ByteArrayOutputStream out = pipeStdoutToStream(); + int exitCode = executeCommand("inline-sign", key.getAbsolutePath(), + "--with-key-password", password.getAbsolutePath()); + + assertEquals(SOPGPException.KeyIsProtected.EXIT_CODE, exitCode); + assertEquals(0, out.size()); + } +} diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignVerifyCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignVerifyCmdTest.java new file mode 100644 index 00000000..cf350e66 --- /dev/null +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignVerifyCmdTest.java @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.cli.commands; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.Test; +import org.slf4j.LoggerFactory; + +public class RoundTripInlineSignVerifyCmdTest extends CLITest { + + public RoundTripInlineSignVerifyCmdTest() { + super(LoggerFactory.getLogger(RoundTripInlineSignVerifyCmdTest.class)); + } + + @Test + public void encryptAndDecryptAMessage() throws IOException { + // write password file + File password = writeFile("password", "sw0rdf1sh"); + + // generate key + File sigmundKey = pipeStdoutToFile("sigmund.key"); + assertSuccess(executeCommand("generate-key", "--with-key-password=" + password.getAbsolutePath(), + "Sigmund Freud ")); + + // extract cert + File sigmundCert = pipeStdoutToFile("sigmund.cert"); + pipeFileToStdin(sigmundKey); + assertSuccess(executeCommand("extract-cert")); + + // sign message + pipeBytesToStdin("Hello, World!\n".getBytes(StandardCharsets.UTF_8)); + File signedMsg = pipeStdoutToFile("signed.asc"); + assertSuccess(executeCommand("inline-sign", "--with-key-password=" + password.getAbsolutePath(), + sigmundKey.getAbsolutePath())); + + // verify message + File verifyFile = nonExistentFile("verify.txt"); + pipeFileToStdin(signedMsg); + assertSuccess(executeCommand("inline-verify", "--verifications-out", verifyFile.getAbsolutePath(), + sigmundCert.getAbsolutePath())); + + String verifications = readStringFromFile(verifyFile); + assertFalse(verifications.trim().isEmpty()); + } +} diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripSignVerifyCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripSignVerifyCmdTest.java new file mode 100644 index 00000000..9dcb3aca --- /dev/null +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripSignVerifyCmdTest.java @@ -0,0 +1,335 @@ +// SPDX-FileCopyrightText: 2021 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.cli.commands; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.text.ParseException; +import java.util.Date; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.KeyFlag; +import org.pgpainless.key.OpenPgpV4Fingerprint; +import org.pgpainless.key.generation.KeySpec; +import org.pgpainless.key.generation.type.KeyType; +import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve; +import org.pgpainless.key.generation.type.xdh_legacy.XDHLegacySpec; +import org.pgpainless.key.info.KeyRingInfo; +import org.slf4j.LoggerFactory; +import sop.exception.SOPGPException; +import sop.util.UTCUtil; + +public class RoundTripSignVerifyCmdTest extends CLITest { + + public RoundTripSignVerifyCmdTest() { + super(LoggerFactory.getLogger(RoundTripSignVerifyCmdTest.class)); + } + + private static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 9DA0 9423 C9F9 4BA4 CCA3 0951 099B 11BF 296A 373E\n" + + "Comment: Sigmund \n" + + "\n" + + "lFgEY2vzkhYJKwYBBAHaRw8BAQdA+Z2OAFQf0k64Au7hIZfXh/ijclabddvwh7Nh\n" + + "kedJ3ZUAAQCZy5p1cvQvRIWUopHwhnrD/oVAa1dNT/nA3cihQ5gkZBHPtCBTaWdt\n" + + "dW5kIDxzaWdtdW5kQHBncGFpbmxlc3Mub3JnPoiPBBMWCgBBBQJja/OSCRAJmxG/\n" + + "KWo3PhYhBJ2glCPJ+UukzKMJUQmbEb8pajc+Ap4BApsBBRYCAwEABAsJCAcFFQoJ\n" + + "CAsCmQEAACM9AP9APloI2waD5gXsJqzenRVU4n/VmZUvcdUyhlbpab/0HQEAlaTw\n" + + "ZvxVyaf8EMFSJOY+LcgacHaZDHRPA1nS3bIfKwycXQRja/OSEgorBgEEAZdVAQUB\n" + + "AQdA1WL4QKgRxbvzW91ICM6PoICSNh2QHK6j0pIdN/cqXz0DAQgHAAD/bOk3WqbF\n" + + "QAE8xxm0w/KDZzL1N0yPcBQ5z4XKmu77FCgQ04h1BBgWCgAdBQJja/OSAp4BApsM\n" + + "BRYCAwEABAsJCAcFFQoJCAsACgkQCZsRvylqNz6rgQEAzoG6HnPCYi2i2c6/ufuy\n" + + "pBkLby2u1JjD0CWSbrM4dZ0A/j/pI4a9b8LcrZcuY2QwHqsXPAJp8QtOOQN6gTvN\n" + + "WcQNnFgEY2vzkhYJKwYBBAHaRw8BAQdAsxcDCvst/GbWxQvvOpChSvmbqWeuBgm3\n" + + "1vRoujFVFcYAAP9Ww46yfWipb8OivTSX+PvgdUhEeVgxENpsyOQLLhQP/RFziNUE\n" + + "GBYKAH0FAmNr85ICngECmwIFFgIDAQAECwkIBwUVCgkIC18gBBkWCgAGBQJja/OS\n" + + "AAoJENqfQTmGIR3GtsMBAL+b1Zo5giQKJGEyx5aGwAz3AwtGiT6QDS9FH6HyM855\n" + + "AP4uAXDiaNxYTugqnG471jYX/hhJqIROeDGrEIkkAp+qDwAKCRAJmxG/KWo3PhOX\n" + + "AP45LPV6I4+D3h8etdiEA2DVvNcpRA8l4WkNcq4q8H1SjwD/c/rX3FCUIWLlAHoR\n" + + "WxCFj+gDgqDNLzwoA4iNo1VMtQc=\n" + + "=/Np6\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + + private static final String CERT = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 9DA0 9423 C9F9 4BA4 CCA3 0951 099B 11BF 296A 373E\n" + + "Comment: Sigmund \n" + + "\n" + + "mDMEY2vzkhYJKwYBBAHaRw8BAQdA+Z2OAFQf0k64Au7hIZfXh/ijclabddvwh7Nh\n" + + "kedJ3ZW0IFNpZ211bmQgPHNpZ211bmRAcGdwYWlubGVzcy5vcmc+iI8EExYKAEEF\n" + + "AmNr85IJEAmbEb8pajc+FiEEnaCUI8n5S6TMowlRCZsRvylqNz4CngECmwEFFgID\n" + + "AQAECwkIBwUVCgkICwKZAQAAIz0A/0A+WgjbBoPmBewmrN6dFVTif9WZlS9x1TKG\n" + + "Vulpv/QdAQCVpPBm/FXJp/wQwVIk5j4tyBpwdpkMdE8DWdLdsh8rDLg4BGNr85IS\n" + + "CisGAQQBl1UBBQEBB0DVYvhAqBHFu/Nb3UgIzo+ggJI2HZAcrqPSkh039ypfPQMB\n" + + "CAeIdQQYFgoAHQUCY2vzkgKeAQKbDAUWAgMBAAQLCQgHBRUKCQgLAAoJEAmbEb8p\n" + + "ajc+q4EBAM6Buh5zwmItotnOv7n7sqQZC28trtSYw9Alkm6zOHWdAP4/6SOGvW/C\n" + + "3K2XLmNkMB6rFzwCafELTjkDeoE7zVnEDbgzBGNr85IWCSsGAQQB2kcPAQEHQLMX\n" + + "Awr7Lfxm1sUL7zqQoUr5m6lnrgYJt9b0aLoxVRXGiNUEGBYKAH0FAmNr85ICngEC\n" + + "mwIFFgIDAQAECwkIBwUVCgkIC18gBBkWCgAGBQJja/OSAAoJENqfQTmGIR3GtsMB\n" + + "AL+b1Zo5giQKJGEyx5aGwAz3AwtGiT6QDS9FH6HyM855AP4uAXDiaNxYTugqnG47\n" + + "1jYX/hhJqIROeDGrEIkkAp+qDwAKCRAJmxG/KWo3PhOXAP45LPV6I4+D3h8etdiE\n" + + "A2DVvNcpRA8l4WkNcq4q8H1SjwD/c/rX3FCUIWLlAHoRWxCFj+gDgqDNLzwoA4iN\n" + + "o1VMtQc=\n" + + "=KuJ4\n" + + "-----END PGP PUBLIC KEY BLOCK-----"; + private static final String PLAINTEXT = "Hello, World!\n"; + private static final String BINARY_SIG = "-----BEGIN PGP SIGNATURE-----\n" + + "Version: PGPainless\n" + + "\n" + + "iHUEABYKACcFAmNr9BgJENqfQTmGIR3GFiEEREwQqwEe+EJMg/Cp2p9BOYYhHcYA\n" + + "AKocAP48P2C3TU33T3Zy73clw0eBa1oW9pwxTGuFxhgOBzmoSwEArj0781GlpTB0\n" + + "Vnr2PjPYEqzB+ZuOzOnGhsVGob4c3Ao=\n" + + "=VWAZ\n" + + "-----END PGP SIGNATURE-----"; + private static final String BINARY_SIG_VERIFICATION = + "2022-11-09T18:40:24Z 444C10AB011EF8424C83F0A9DA9F413986211DC6 9DA09423C9F94BA4CCA30951099B11BF296A373E mode:binary\n"; + private static final String TEXT_SIG = "-----BEGIN PGP SIGNATURE-----\n" + + "Version: PGPainless\n" + + "\n" + + "iHUEARYKACcFAmNr9E4JENqfQTmGIR3GFiEEREwQqwEe+EJMg/Cp2p9BOYYhHcYA\n" + + "AG+CAQD1B3GAAlyxahSiGhvJv7YAI1m6qGcI7dIXcV7FkAFPSgEAlZ0UpCC8oGR+\n" + + "hi/mQlex4z0hDWSA4abAjclPTJ+qkAI=\n" + + "=s5xn\n" + + "-----END PGP SIGNATURE-----"; + private static final String TEXT_SIG_VERIFICATION = + "2022-11-09T18:41:18Z 444C10AB011EF8424C83F0A9DA9F413986211DC6 9DA09423C9F94BA4CCA30951099B11BF296A373E mode:text\n"; + private static final Date TEXT_SIG_CREATION; + + static { + try { + TEXT_SIG_CREATION = UTCUtil.parseUTCDate("2022-11-09T18:41:18Z"); + } catch (ParseException e) { + throw new RuntimeException(e); + } + } + + @Test + public void createArmoredSignature() throws IOException { + File keyFile = writeFile("key.asc", KEY); + pipeStringToStdin(PLAINTEXT); + ByteArrayOutputStream out = pipeStdoutToStream(); + assertSuccess(executeCommand("sign", "--as", "text", keyFile.getAbsolutePath())); + assertTrue(out.toString().startsWith("-----BEGIN PGP SIGNATURE-----\n")); + } + + @Test + public void createUnarmoredSignature() throws IOException { + File keyFile = writeFile("key.asc", KEY); + pipeStringToStdin(PLAINTEXT); + ByteArrayOutputStream out = pipeStdoutToStream(); + assertSuccess(executeCommand("sign", "--no-armor", keyFile.getAbsolutePath())); + assertFalse(out.toString().startsWith("-----BEGIN PGP SIGNATURE-----\n")); + } + + @Test + public void unarmorArmoredSigAndVerify() throws IOException { + File certFile = writeFile("cert.asc", CERT); + + pipeStringToStdin(BINARY_SIG); + File unarmoredSigFile = pipeStdoutToFile("sig.pgp"); + assertSuccess(executeCommand("dearmor")); + + pipeStringToStdin(PLAINTEXT); + ByteArrayOutputStream out = pipeStdoutToStream(); + assertSuccess(executeCommand("verify", unarmoredSigFile.getAbsolutePath(), certFile.getAbsolutePath())); + + assertEquals(BINARY_SIG_VERIFICATION, out.toString()); + } + + @Test + public void testNotBefore() throws IOException { + File cert = writeFile("cert.asc", CERT); + File sigFile = writeFile("sig.asc", TEXT_SIG); + Date plus1Minute = new Date(TEXT_SIG_CREATION.getTime() + 1000 * 60); + + pipeStringToStdin(PLAINTEXT); + ByteArrayOutputStream out = pipeStdoutToStream(); + int exitCode = executeCommand("verify", sigFile.getAbsolutePath(), cert.getAbsolutePath(), + "--not-before", UTCUtil.formatUTCDate(plus1Minute)); + + assertEquals(SOPGPException.NoSignature.EXIT_CODE, exitCode); + assertEquals(0, out.size()); + + Date minus1Minute = new Date(TEXT_SIG_CREATION.getTime() - 1000 * 60); + pipeStringToStdin(PLAINTEXT); + out = pipeStdoutToStream(); + exitCode = executeCommand("verify", sigFile.getAbsolutePath(), cert.getAbsolutePath(), + "--not-before", UTCUtil.formatUTCDate(minus1Minute)); + + assertSuccess(exitCode); + assertEquals(TEXT_SIG_VERIFICATION, out.toString()); + } + + @Test + public void testNotAfter() throws IOException { + File cert = writeFile("cert.asc", CERT); + File sigFile = writeFile("sig.asc", TEXT_SIG); + + Date minus1Minute = new Date(TEXT_SIG_CREATION.getTime() - 1000 * 60); + pipeStringToStdin(PLAINTEXT); + ByteArrayOutputStream out = pipeStdoutToStream(); + int exitCode = executeCommand("verify", sigFile.getAbsolutePath(), cert.getAbsolutePath(), + "--not-after", UTCUtil.formatUTCDate(minus1Minute)); + + assertEquals(SOPGPException.NoSignature.EXIT_CODE, exitCode); + assertEquals(0, out.size()); + + Date plus1Minute = new Date(TEXT_SIG_CREATION.getTime() + 1000 * 60); + pipeStringToStdin(PLAINTEXT); + out = pipeStdoutToStream(); + exitCode = executeCommand("verify", sigFile.getAbsolutePath(), cert.getAbsolutePath(), + "--not-after", UTCUtil.formatUTCDate(plus1Minute)); + + assertSuccess(exitCode); + assertEquals(TEXT_SIG_VERIFICATION, out.toString()); + } + + @Test + public void testSignWithIncapableKey() + throws IOException, PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() + .addUserId("Cannot Sign ") + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER)) + .addSubkey(KeySpec.getBuilder(KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)) + .build(); + File keyFile = writeFile("key.pgp", secretKeys.getEncoded()); + + pipeStringToStdin("Hello, World!\n"); + ByteArrayOutputStream out = pipeStdoutToStream(); + int exitCode = executeCommand("sign", keyFile.getAbsolutePath()); + + assertEquals(SOPGPException.KeyCannotSign.EXIT_CODE, exitCode); + assertEquals(0, out.size()); + } + + @Test + public void testSignatureCreationAndVerification() + throws IOException { + // Create key and cert + File aliceKeyFile = pipeStdoutToFile("alice.key"); + assertSuccess(executeCommand("generate-key", "Alice ")); + File aliceCertFile = pipeStdoutToFile("alice.cert"); + pipeFileToStdin(aliceKeyFile); + assertSuccess(executeCommand("extract-cert")); + + File micalgOut = nonExistentFile("micalg"); + String msg = "If privacy is outlawed, only outlaws will have privacy.\n"; + File dataFile = writeFile("data", msg); + + // sign data + File sigFile = pipeStdoutToFile("sig.asc"); + pipeFileToStdin(dataFile); + assertSuccess(executeCommand("sign", + "--armor", + "--as", "binary", + "--micalg-out", micalgOut.getAbsolutePath(), + aliceKeyFile.getAbsolutePath())); + + // verify test data signature + pipeFileToStdin(dataFile); + ByteArrayOutputStream verificationsOut = pipeStdoutToStream(); + assertSuccess(executeCommand("verify", sigFile.getAbsolutePath(), aliceCertFile.getAbsolutePath())); + + // Test verification output + + PGPPublicKeyRing cert = PGPainless.readKeyRing().publicKeyRing(readBytesFromFile(aliceCertFile)); + KeyRingInfo info = PGPainless.inspectKeyRing(cert); + + // [date] [signing-key-fp] [primary-key-fp] signed by [key.pub] + String verification = verificationsOut.toString(); + String[] split = verification.split(" "); + OpenPgpV4Fingerprint primaryKeyFingerprint = new OpenPgpV4Fingerprint(cert); + OpenPgpV4Fingerprint signingKeyFingerprint = new OpenPgpV4Fingerprint(info.getSigningSubkeys().get(0)); + assertEquals(signingKeyFingerprint.toString(), split[1].trim(), verification); + assertEquals(primaryKeyFingerprint.toString(), split[2].trim()); + + // Test micalg output + String content = readStringFromFile(micalgOut); + assertEquals("pgp-sha512", content); + } + + private static final String PROTECTED_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 738E EAB2 503D 322D 613A C42A B18E 8BF8 884F C050\n" + + "Comment: Axel \n" + + "\n" + + "lIYEY2v6aRYJKwYBBAHaRw8BAQdA3PXtH19zYpVQ9zTU3zlY+iXUptelAO3z4vK/\n" + + "M2FkmrP+CQMCYgVa6K+InVJguITSDIA+HQ6vhOZ5Dbanqx7GFbJbJLD2fWrxhTSr\n" + + "BUWGaUWTqN647auD/kUI8phH1cedVL6CzVR+YWvaWj9zZHr/CYXLobQaQXhlbCA8\n" + + "YXhlbEBwZ3BhaW5sZXNzLm9yZz6IjwQTFgoAQQUCY2v6aQkQsY6L+IhPwFAWIQRz\n" + + "juqyUD0yLWE6xCqxjov4iE/AUAKeAQKbAQUWAgMBAAQLCQgHBRUKCQgLApkBAACq\n" + + "zgEAkxB+dUI7Jjcg5zRvT1EfE9DKCI1qTsxOAU/ZXLcSXLkBAJtWRsyptetZvjzB\n" + + "Ze2A7ArOl4q+IvKvun/d783YyRMInIsEY2v6aRIKKwYBBAGXVQEFAQEHQPFmlZ+o\n" + + "jCGEo2X0474vJfRG7blctuZXmCbC0sLO7MgzAwEIB/4JAwJiBVror4idUmDFhBq4\n" + + "lEhJxjCVc6aSD6+EWRT3YdplqCmNdynnrPombUFst6LfJFzns3H3d0rCeXHfQr93\n" + + "GrHTLkHfW8G3x0PJJPiqFkBviHUEGBYKAB0FAmNr+mkCngECmwwFFgIDAQAECwkI\n" + + "BwUVCgkICwAKCRCxjov4iE/AUNC2AP9WDx4lHt9oYFLSrM8vMLRFI31U8TkYrtCe\n" + + "pYICE76cIAEA5+wEbtE5vQrLxOqIRueVVdzwK9kTeMvSIQfc9PNoyQKchgRja/pp\n" + + "FgkrBgEEAdpHDwEBB0CyAEVlCUbFr3dBBG3MQ84hjCPfYqSx9kYsTN8j5Og6uP4J\n" + + "AwJiBVror4idUmCIFuAYXia0YpEhEpB/Lrn/D6/WAUPEgZjNLMvJzL//EmhkWfEa\n" + + "OfQz/fslj1erWNjLKNiW5C/TvGapDfjbn596AkNlcd1JiNUEGBYKAH0FAmNr+mkC\n" + + "ngECmwIFFgIDAQAECwkIBwUVCgkIC18gBBkWCgAGBQJja/ppAAoJELRgil1uCuQj\n" + + "VUYBAJecbedwwqWQITVqucEBIraTRoc6ZGkN8jytDp8z9CsBAQDrb/W/J/kze6ln\n" + + "nRyJSriWF3SjcKOGIRkUslmdJEPPCQAKCRCxjov4iE/AUAvbAQDBBgQFG8acTT5L\n" + + "cyIi1Ix9/XBG7G23SSs6l7Beap8M+wEAmK13NYuq7Mv/mct8iIKZbBFH9aAiY+nX\n" + + "3Uct4Q5f0w0=\n" + + "=K65R\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + private static final String PASSPHRASE = "orange"; + private static final String SIGNING_KEY = "9846F3606EE875FB77EC8808B4608A5D6E0AE423 738EEAB2503D322D613AC42AB18E8BF8884FC050"; + + @Test + public void signWithProtectedKey_missingPassphraseFails() throws IOException { + File key = writeFile("key.asc", PROTECTED_KEY); + pipeStringToStdin(PLAINTEXT); + ByteArrayOutputStream out = pipeStdoutToStream(); + int exitCode = executeCommand("sign", key.getAbsolutePath()); + assertEquals(SOPGPException.KeyIsProtected.EXIT_CODE, exitCode); + assertEquals(0, out.size()); + } + + @Test + public void signWithProtectedKey_wrongPassphraseFails() throws IOException { + File password = writeFile("password", "blue"); + File key = writeFile("key.asc", PROTECTED_KEY); + pipeStringToStdin(PLAINTEXT); + ByteArrayOutputStream out = pipeStdoutToStream(); + int exitCode = executeCommand("sign", key.getAbsolutePath(), + "--with-key-password", password.getAbsolutePath()); + assertEquals(SOPGPException.KeyIsProtected.EXIT_CODE, exitCode); + assertEquals(0, out.size()); + } + + @Test + public void signWithProtectedKey() throws IOException { + File password = writeFile("password", PASSPHRASE); + File key = writeFile("key.asc", PROTECTED_KEY); + pipeStringToStdin(PROTECTED_KEY); + File cert = pipeStdoutToFile("cert.asc"); + assertSuccess(executeCommand("extract-cert")); + + pipeStringToStdin(PLAINTEXT); + File sigFile = pipeStdoutToFile("sig.asc"); + assertSuccess(executeCommand("sign", key.getAbsolutePath(), + "--with-key-password", password.getAbsolutePath())); + + pipeStringToStdin(PLAINTEXT); + ByteArrayOutputStream verificationOut = pipeStdoutToStream(); + assertSuccess(executeCommand("verify", sigFile.getAbsolutePath(), cert.getAbsolutePath())); + assertTrue(verificationOut.toString().contains(SIGNING_KEY)); + } + +} diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/SignVerifyTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/SignVerifyTest.java deleted file mode 100644 index cd2adc89..00000000 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/SignVerifyTest.java +++ /dev/null @@ -1,119 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.cli.commands; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.PrintStream; -import java.nio.charset.StandardCharsets; -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; -import java.util.Date; - -import com.ginsberg.junit.exit.FailOnSystemExit; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.util.io.Streams; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.pgpainless.PGPainless; -import org.pgpainless.cli.PGPainlessCLI; -import org.pgpainless.cli.TestUtils; -import org.pgpainless.key.OpenPgpV4Fingerprint; -import org.pgpainless.key.info.KeyRingInfo; -import org.pgpainless.key.util.KeyRingUtils; - -public class SignVerifyTest { - - private static File tempDir; - private static PrintStream originalSout; - - private final String data = "If privacy is outlawed, only outlaws will have privacy.\n"; - - @BeforeAll - public static void prepare() throws IOException { - tempDir = TestUtils.createTempDirectory(); - } - - @Test - @FailOnSystemExit - public void testSignatureCreationAndVerification() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { - originalSout = System.out; - InputStream originalIn = System.in; - - // Write alice key to disc - File aliceKeyFile = new File(tempDir, "alice.key"); - assertTrue(aliceKeyFile.createNewFile()); - PGPSecretKeyRing aliceKeys = PGPainless.generateKeyRing() - .modernKeyRing("alice", null); - OutputStream aliceKeyOut = new FileOutputStream(aliceKeyFile); - Streams.pipeAll(new ByteArrayInputStream(aliceKeys.getEncoded()), aliceKeyOut); - aliceKeyOut.close(); - - // Write alice pub key to disc - File aliceCertFile = new File(tempDir, "alice.pub"); - assertTrue(aliceCertFile.createNewFile()); - PGPPublicKeyRing alicePub = KeyRingUtils.publicKeyRingFrom(aliceKeys); - OutputStream aliceCertOut = new FileOutputStream(aliceCertFile); - Streams.pipeAll(new ByteArrayInputStream(alicePub.getEncoded()), aliceCertOut); - aliceCertOut.close(); - - // Write test data to disc - File dataFile = new File(tempDir, "data"); - assertTrue(dataFile.createNewFile()); - FileOutputStream dataOut = new FileOutputStream(dataFile); - Streams.pipeAll(new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8)), dataOut); - dataOut.close(); - - // Sign test data - FileInputStream dataIn = new FileInputStream(dataFile); - System.setIn(dataIn); - File sigFile = new File(tempDir, "sig.asc"); - assertTrue(sigFile.createNewFile()); - FileOutputStream sigOut = new FileOutputStream(sigFile); - System.setOut(new PrintStream(sigOut)); - PGPainlessCLI.execute("sign", "--armor", aliceKeyFile.getAbsolutePath()); - sigOut.close(); - - // verify test data signature - ByteArrayOutputStream verifyOut = new ByteArrayOutputStream(); - System.setOut(new PrintStream(verifyOut)); - dataIn = new FileInputStream(dataFile); - System.setIn(dataIn); - PGPainlessCLI.execute("verify", sigFile.getAbsolutePath(), aliceCertFile.getAbsolutePath()); - dataIn.close(); - - // Test verification output - - // [date] [signing-key-fp] [primary-key-fp] signed by [key.pub] - String verification = verifyOut.toString(); - String[] split = verification.split(" "); - OpenPgpV4Fingerprint primaryKeyFingerprint = new OpenPgpV4Fingerprint(aliceKeys); - OpenPgpV4Fingerprint signingKeyFingerprint = new OpenPgpV4Fingerprint(new KeyRingInfo(alicePub, new Date()).getSigningSubkeys().get(0)); - assertEquals(signingKeyFingerprint.toString(), split[1].trim()); - assertEquals(primaryKeyFingerprint.toString(), split[2].trim()); - - System.setIn(originalIn); - } - - @AfterAll - public static void after() { - System.setOut(originalSout); - // CHECKSTYLE:OFF - System.out.println(tempDir.getAbsolutePath()); - // CHECKSTYLE:ON - } -} diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/VersionCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/VersionCmdTest.java new file mode 100644 index 00000000..e0915920 --- /dev/null +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/VersionCmdTest.java @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.cli.commands; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import org.junit.jupiter.api.Test; +import org.slf4j.LoggerFactory; + +public class VersionCmdTest extends CLITest { + + public VersionCmdTest() { + super(LoggerFactory.getLogger(VersionCmdTest.class)); + } + + @Test + public void testVersion() throws IOException { + ByteArrayOutputStream out = pipeStdoutToStream(); + assertSuccess(executeCommand("version")); + assertTrue(out.toString().startsWith("PGPainless-SOP ")); + } + + @Test + public void testGetBackendVersion() throws IOException { + ByteArrayOutputStream out = pipeStdoutToStream(); + assertSuccess(executeCommand("version", "--backend")); + assertTrue(out.toString().startsWith("PGPainless ")); + } + + @Test + public void testExtendedVersion() throws IOException { + ByteArrayOutputStream out = pipeStdoutToStream(); + assertSuccess(executeCommand("version", "--extended")); + String info = out.toString(); + assertTrue(info.startsWith("PGPainless-SOP ")); + assertTrue(info.contains("Bouncy Castle")); + assertTrue(info.contains("Stateless OpenPGP Protocol")); + } + + @Test + public void testSopSpecVersion() throws IOException { + ByteArrayOutputStream out = pipeStdoutToStream(); + assertSuccess(executeCommand("version", "--sop-spec")); + String info = out.toString(); + assertTrue(info.startsWith("draft-dkg-openpgp-stateless-cli-")); + } +} diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/misc/SignUsingPublicKeyBehaviorTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/misc/SignUsingPublicKeyBehaviorTest.java new file mode 100644 index 00000000..d6065b82 --- /dev/null +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/misc/SignUsingPublicKeyBehaviorTest.java @@ -0,0 +1,102 @@ +// SPDX-FileCopyrightText: 2021 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.cli.misc; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.File; +import java.io.IOException; + +import org.junit.jupiter.api.Test; +import org.pgpainless.cli.commands.CLITest; +import org.slf4j.LoggerFactory; +import sop.exception.SOPGPException; + +public class SignUsingPublicKeyBehaviorTest extends CLITest { + + public static final String KEY_THAT_IS_A_CERT = "" + + "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "\n" + + "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\n" + + "ltx3QXLHQw==\n" + + "=oJQ2\n" + + "-----END PGP PUBLIC KEY BLOCK-----"; + + public SignUsingPublicKeyBehaviorTest() { + super(LoggerFactory.getLogger(SignUsingPublicKeyBehaviorTest.class)); + } + + @Test + public void testSignatureCreationAndVerification() throws IOException { + // Write alice key to disc + File aliceKeyFile = writeFile("alice.key", KEY_THAT_IS_A_CERT); + + // Write test data to disc + File dataFile = writeFile("data", "If privacy is outlawed, only outlaws will have privacy.\n"); + + // Sign test data + File sigFile = pipeStdoutToFile("sig.asc"); + pipeFileToStdin(dataFile); + assertEquals(SOPGPException.KeyCannotSign.EXIT_CODE, + executeCommand("sign", "--armor", aliceKeyFile.getAbsolutePath())); + + assertTrue(readStringFromFile(sigFile).trim().isEmpty()); + } +} diff --git a/pgpainless-cli/src/test/resources/logback-test.xml b/pgpainless-cli/src/test/resources/logback-test.xml new file mode 100644 index 00000000..abb1b8fd --- /dev/null +++ b/pgpainless-cli/src/test/resources/logback-test.xml @@ -0,0 +1,25 @@ + + + + + System.err + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + System.out + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/pgpainless-core/README.md b/pgpainless-core/README.md index 90bc0b83..82afdfb5 100644 --- a/pgpainless-core/README.md +++ b/pgpainless-core/README.md @@ -6,4 +6,51 @@ SPDX-License-Identifier: Apache-2.0 # PGPainless-Core -Wrapper around Bouncycastle's OpenPGP implementation. \ No newline at end of file +[![javadoc](https://javadoc.io/badge2/org.pgpainless/pgpainless-core/javadoc.svg)](https://javadoc.io/doc/org.pgpainless/pgpainless-core) +[![Maven Central](https://badgen.net/maven/v/maven-central/org.pgpainless/pgpainless-core)](https://search.maven.org/artifact/org.pgpainless/pgpainless-core) + +Wrapper around Bouncy Castle's OpenPGP implementation. + +## Protection Against Attacks + +PGPainless aims to fulfill the primary goals of cryptography: +* Confidentiality through message encryption +* Authenticity through signatures +* Integrity through the use of Modification Detection Code and again signatures + +In short: Communication protected using PGPainless is intended to be private, +users can verify that messages they receive were really send by their communication peer +and users can verify that messages have not been tampered with. + +This is being achieved by preventing a number of typical attacks on the user's communication, +like the attacker introducing an evil subkey to the victims public key, or the attacker creating +counterfeit signatures to fool the victim. + +Due to its nature as a library however, it does not make sense to set up defences against all possible +attack types (see below). +So here is a threat model that best applies to PGPainless. + +### Threat Model +A threat model that makes the most sense for PGPainless would be an evil attacker using PGPainless +through a benign client application (like an email app) on a trustworthy device. + +The attacker can try to feed the application malicious input (like manipulated public key updates, +specially crafted PGP message objects etc.) but they cannot access the victims decrypted secret key material as +it is protected by the device (e.g. stored in a secure key store). + +### What doesn't PGPainless Protect Against? + +#### Brute Force Attacks +It was decided that protection against brute force attacks on passwords used in symmetric encryption +(password encrypted messages/keys) are out of scope for PGPainless. +PGPainless cannot limit access to the ciphertext that is being brute forced, as that is provided by +the application that uses PGPainless. +Therefore, protection against brute force attacks must be employed by the application itself. + +#### (Public) Key Modification Attacks +As a library, PGPainless cannot protect against swapped out public keys. +It is therefore responsibility of the consumer to ensure that an attacker on the same system cannot tamper with stored keys. +It is highly advised to store both secret and public keys in a secure key storage which protects against modifications. + +Furthermore, PGPainless cannot verify key authenticity, so it is up to the application that uses PGPainless to check, +if a key really belongs to a certain user. diff --git a/pgpainless-core/build.gradle b/pgpainless-core/build.gradle index 61a3fda6..64d538d5 100644 --- a/pgpainless-core/build.gradle +++ b/pgpainless-core/build.gradle @@ -7,16 +7,31 @@ plugins { } dependencies { + // JUnit testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion" testImplementation "org.junit.jupiter:junit-jupiter-params:$junitVersion" testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion" - testImplementation 'ch.qos.logback:logback-classic:1.2.5' + // Mocking Components + testImplementation "org.mockito:mockito-core:$mockitoVersion" + + // Logging api "org.slf4j:slf4j-api:$slf4jVersion" + testImplementation "ch.qos.logback:logback-classic:$logbackVersion" - api "org.bouncycastle:bcprov-jdk15on:$bouncyCastleVersion" - api "org.bouncycastle:bcpg-jdk15on:$bouncyCastleVersion" + // Bouncy Castle + api "org.bouncycastle:bcprov-jdk18on:$bouncyCastleVersion" + api "org.bouncycastle:bcpg-jdk18on:$bouncyPgVersion" + api "org.bouncycastle:bcutil-jdk18on:$bouncyCastleVersion" + // api(files("../libs/bcpg-jdk18on-1.70.jar")) - // https://mvnrepository.com/artifact/com.google.code.findbugs/jsr305 - implementation group: 'com.google.code.findbugs', name: 'jsr305', version: '3.0.2' + // @Nullable, @Nonnull annotations + implementation "com.google.code.findbugs:jsr305:3.0.2" +} + +// https://docs.gradle.org/current/userguide/java_library_plugin.html#sec:java_library_modular_auto +tasks.named('jar') { + manifest { + attributes('Automatic-Module-Name': 'org.pgpainless.core') + } } diff --git a/pgpainless-core/src/main/java/org/gnupg/GnuPGDummyExtension.java b/pgpainless-core/src/main/java/org/gnupg/GnuPGDummyExtension.java new file mode 100644 index 00000000..d744e222 --- /dev/null +++ b/pgpainless-core/src/main/java/org/gnupg/GnuPGDummyExtension.java @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.gnupg; + +import org.bouncycastle.bcpg.S2K; + +public enum GnuPGDummyExtension { + + /** + * Do not store the secret part at all. + */ + NO_PRIVATE_KEY(S2K.GNU_PROTECTION_MODE_NO_PRIVATE_KEY), + + /** + * A stub to access smartcards. + */ + DIVERT_TO_CARD(S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD), + ; + + private final int id; + + GnuPGDummyExtension(int id) { + this.id = id; + } + + public int getId() { + return id; + } +} diff --git a/pgpainless-core/src/main/java/org/gnupg/GnuPGDummyKeyUtil.java b/pgpainless-core/src/main/java/org/gnupg/GnuPGDummyKeyUtil.java new file mode 100644 index 00000000..42af92d8 --- /dev/null +++ b/pgpainless-core/src/main/java/org/gnupg/GnuPGDummyKeyUtil.java @@ -0,0 +1,210 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.gnupg; + +import org.bouncycastle.bcpg.PublicKeyPacket; +import org.bouncycastle.bcpg.S2K; +import org.bouncycastle.bcpg.SecretKeyPacket; +import org.bouncycastle.bcpg.SecretSubkeyPacket; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.pgpainless.key.SubkeyIdentifier; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * This class can be used to remove private keys from secret software-keys by replacing them with + * stub secret keys in the style of GnuPGs proprietary extensions. + * + * @see + * GnuPGs doc/DETAILS - GNU extensions to the S2K algorithm + */ +public final class GnuPGDummyKeyUtil { + + private GnuPGDummyKeyUtil() { + + } + + /** + * Return the key-ids of all keys which appear to be stored on a hardware token / smartcard by GnuPG. + * Note, that this functionality is based on GnuPGs proprietary S2K extensions, which are not strictly required + * for dealing with hardware-backed keys. + * + * @param secretKeys secret keys + * @return set of keys with S2K type GNU_DUMMY_S2K and protection mode DIVERT_TO_CARD + */ + public static Set getIdsOfKeysWithGnuPGS2KDivertedToCard(@Nonnull PGPSecretKeyRing secretKeys) { + Set hardwareBackedKeys = new HashSet<>(); + for (PGPSecretKey secretKey : secretKeys) { + S2K s2K = secretKey.getS2K(); + if (s2K == null) { + continue; + } + + int type = s2K.getType(); + int mode = s2K.getProtectionMode(); + // TODO: Is GNU_DUMMY_S2K appropriate? + if (type == S2K.GNU_DUMMY_S2K && mode == S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD) { + SubkeyIdentifier hardwareBackedKey = new SubkeyIdentifier(secretKeys, secretKey.getKeyID()); + hardwareBackedKeys.add(hardwareBackedKey); + } + } + return hardwareBackedKeys; + } + + /** + * Modify the given {@link PGPSecretKeyRing}. + * + * @param secretKeys secret keys + * @return builder + */ + public static Builder modify(@Nonnull PGPSecretKeyRing secretKeys) { + return new Builder(secretKeys); + } + + public static final class Builder { + + private final PGPSecretKeyRing keys; + + private Builder(@Nonnull PGPSecretKeyRing keys) { + this.keys = keys; + } + + /** + * Remove all private keys that match the given {@link KeyFilter} from the key ring and replace them with + * GNU_DUMMY keys with S2K protection mode {@link GnuPGDummyExtension#NO_PRIVATE_KEY}. + * + * @param filter filter to select keys for removal + * @return modified key ring + */ + public PGPSecretKeyRing removePrivateKeys(@Nonnull KeyFilter filter) { + return replacePrivateKeys(GnuPGDummyExtension.NO_PRIVATE_KEY, null, filter); + } + + /** + * Remove all private keys that match the given {@link KeyFilter} from the key ring and replace them with + * GNU_DUMMY keys with S2K protection mode {@link GnuPGDummyExtension#DIVERT_TO_CARD}. + * This method will set the serial number of the card to 0x00000000000000000000000000000000. + * NOTE: This method does not actually move any keys to a card. + * + * @param filter filter to select keys for removal + * @return modified key ring + */ + public PGPSecretKeyRing divertPrivateKeysToCard(@Nonnull KeyFilter filter) { + return divertPrivateKeysToCard(filter, new byte[16]); + } + + /** + * Remove all private keys that match the given {@link KeyFilter} from the key ring and replace them with + * GNU_DUMMY keys with S2K protection mode {@link GnuPGDummyExtension#DIVERT_TO_CARD}. + * This method will include the card serial number into the encoded dummy key. + * NOTE: This method does not actually move any keys to a card. + * + * @param filter filter to select keys for removal + * @param cardSerialNumber serial number of the card (at most 16 bytes long) + * @return modified key ring + */ + public PGPSecretKeyRing divertPrivateKeysToCard(@Nonnull KeyFilter filter, @Nullable byte[] cardSerialNumber) { + if (cardSerialNumber != null && cardSerialNumber.length > 16) { + throw new IllegalArgumentException("Card serial number length cannot exceed 16 bytes."); + } + return replacePrivateKeys(GnuPGDummyExtension.DIVERT_TO_CARD, cardSerialNumber, filter); + } + + private PGPSecretKeyRing replacePrivateKeys(@Nonnull GnuPGDummyExtension extension, + @Nullable byte[] serial, + @Nonnull KeyFilter filter) { + byte[] encodedSerial = serial != null ? encodeSerial(serial) : null; + S2K s2k = extensionToS2K(extension); + + List secretKeyList = new ArrayList<>(); + for (PGPSecretKey secretKey : keys) { + if (!filter.filter(secretKey.getKeyID())) { + // No conversion, do not modify subkey + secretKeyList.add(secretKey); + continue; + } + + PublicKeyPacket publicKeyPacket = secretKey.getPublicKey().getPublicKeyPacket(); + if (secretKey.isMasterKey()) { + SecretKeyPacket keyPacket = new SecretKeyPacket(publicKeyPacket, + 0, SecretKeyPacket.USAGE_SHA1, s2k, null, encodedSerial); + PGPSecretKey onCard = new PGPSecretKey(keyPacket, secretKey.getPublicKey()); + secretKeyList.add(onCard); + } else { + SecretSubkeyPacket keyPacket = new SecretSubkeyPacket(publicKeyPacket, + 0, SecretKeyPacket.USAGE_SHA1, s2k, null, encodedSerial); + PGPSecretKey onCard = new PGPSecretKey(keyPacket, secretKey.getPublicKey()); + secretKeyList.add(onCard); + } + } + + return new PGPSecretKeyRing(secretKeyList); + } + + private byte[] encodeSerial(@Nonnull byte[] serial) { + byte[] encoded = new byte[serial.length + 1]; + encoded[0] = (byte) (serial.length & 0xff); + System.arraycopy(serial, 0, encoded, 1, serial.length); + return encoded; + } + + private S2K extensionToS2K(@Nonnull GnuPGDummyExtension extension) { + return S2K.gnuDummyS2K(extension == GnuPGDummyExtension.DIVERT_TO_CARD ? + S2K.GNUDummyParams.divertToCard() : S2K.GNUDummyParams.noPrivateKey()); + } + } + + /** + * Filter for selecting keys. + */ + @FunctionalInterface + public interface KeyFilter { + + /** + * Return true, if the given key should be selected, false otherwise. + * + * @param keyId id of the key + * @return select + */ + boolean filter(long keyId); + + /** + * Select any key. + * + * @return filter + */ + static KeyFilter any() { + return keyId -> true; + } + + /** + * Select only the given keyId. + * + * @param onlyKeyId only acceptable key id + * @return filter + */ + static KeyFilter only(long onlyKeyId) { + return keyId -> keyId == onlyKeyId; + } + + /** + * Select all keyIds which are contained in the given set of ids. + * + * @param ids set of acceptable keyIds + * @return filter + */ + static KeyFilter selected(Collection ids) { + // noinspection Convert2MethodRef + return keyId -> ids.contains(keyId); + } + } +} diff --git a/pgpainless-core/src/main/java/org/gnupg/package-info.java b/pgpainless-core/src/main/java/org/gnupg/package-info.java new file mode 100644 index 00000000..03268619 --- /dev/null +++ b/pgpainless-core/src/main/java/org/gnupg/package-info.java @@ -0,0 +1,8 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +/** + * Utility classes related to creating keys with GNU DUMMY S2K values. + */ +package org.gnupg; diff --git a/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java b/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java deleted file mode 100644 index 7d5ca4b0..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java +++ /dev/null @@ -1,149 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless; - -import java.io.IOException; -import java.util.Date; -import javax.annotation.Nonnull; - -import org.bouncycastle.openpgp.PGPKeyRing; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.pgpainless.decryption_verification.DecryptionBuilder; -import org.pgpainless.decryption_verification.DecryptionStream; -import org.pgpainless.encryption_signing.EncryptionBuilder; -import org.pgpainless.encryption_signing.EncryptionStream; -import org.pgpainless.key.generation.KeyRingBuilder; -import org.pgpainless.key.info.KeyRingInfo; -import org.pgpainless.key.modification.secretkeyring.SecretKeyRingEditor; -import org.pgpainless.key.modification.secretkeyring.SecretKeyRingEditorInterface; -import org.pgpainless.key.parsing.KeyRingReader; -import org.pgpainless.key.util.KeyRingUtils; -import org.pgpainless.policy.Policy; -import org.pgpainless.decryption_verification.cleartext_signatures.VerifyCleartextSignatures; -import org.pgpainless.decryption_verification.cleartext_signatures.VerifyCleartextSignaturesImpl; -import org.pgpainless.util.ArmorUtils; - -public final class PGPainless { - - private PGPainless() { - - } - - /** - * Generate a new OpenPGP key ring. - * @return builder - */ - public static KeyRingBuilder generateKeyRing() { - return new KeyRingBuilder(); - } - - /** - * Read an existing OpenPGP key ring. - * @return builder - */ - public static KeyRingReader readKeyRing() { - return new KeyRingReader(); - } - - /** - * Extract a public key certificate from a secret key. - * - * @param secretKey secret key - * @return public key certificate - */ - public static PGPPublicKeyRing extractCertificate(@Nonnull PGPSecretKeyRing secretKey) { - return KeyRingUtils.publicKeyRingFrom(secretKey); - } - - /** - * Wrap a key or certificate in ASCII armor. - * - * @param key key or certificate - * @return ascii armored string - * @throws IOException - */ - public static String asciiArmor(@Nonnull PGPKeyRing key) throws IOException { - if (key instanceof PGPSecretKeyRing) { - return ArmorUtils.toAsciiArmoredString((PGPSecretKeyRing) key); - } else { - return ArmorUtils.toAsciiArmoredString((PGPPublicKeyRing) key); - } - } - - /** - * Create an {@link EncryptionStream}, which can be used to encrypt and/or sign data using OpenPGP. - * - * @return builder - */ - public static EncryptionBuilder encryptAndOrSign() { - return new EncryptionBuilder(); - } - - /** - * Create a {@link DecryptionStream}, which can be used to decrypt and/or verify data using OpenPGP. - * - * @return builder - */ - public static DecryptionBuilder decryptAndOrVerify() { - return new DecryptionBuilder(); - } - - /** - * Verify a cleartext-signed message. - * Cleartext signed messages are often found in emails and look like this: - *
-     * {@code
-     * -----BEGIN PGP SIGNED MESSAGE-----
-     * Hash: [Hash algorithm]
-     * [Human Readable Message Body]
-     * -----BEGIN PGP SIGNATURE-----
-     * [Signature]
-     * -----END PGP SIGNATURE-----
-     * }
-     * 
- * - * @return builder - */ - public static VerifyCleartextSignatures verifyCleartextSignedMessage() { - return new VerifyCleartextSignaturesImpl(); - } - - /** - * Make changes to a key ring. - * This method can be used to change key expiration dates and passphrases, or add/remove/revoke subkeys. - * - * After making the desired changes in the builder, the modified key ring can be extracted using {@link SecretKeyRingEditorInterface#done()}. - * - * @param secretKeys secret key ring - * @return builder - */ - public static SecretKeyRingEditorInterface modifyKeyRing(PGPSecretKeyRing secretKeys) { - return new SecretKeyRingEditor(secretKeys); - } - - /** - * Quickly access information about a {@link org.bouncycastle.openpgp.PGPPublicKeyRing} / {@link PGPSecretKeyRing}. - * This method can be used to determine expiration dates, key flags and other information about a key. - * - * To evaluate a key at a given date (e.g. to determine if the key was allowed to create a certain signature) - * use {@link KeyRingInfo#KeyRingInfo(PGPKeyRing, Date)} instead. - * - * @param keyRing key ring - * @return access object - */ - public static KeyRingInfo inspectKeyRing(PGPKeyRing keyRing) { - return new KeyRingInfo(keyRing); - } - - /** - * Access, and make changes to PGPainless policy on acceptable/default algorithms etc. - * - * @return policy - */ - public static Policy getPolicy() { - return Policy.getInstance(); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/AlgorithmSuite.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/AlgorithmSuite.java deleted file mode 100644 index cc416ea2..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/AlgorithmSuite.java +++ /dev/null @@ -1,63 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.algorithm; - -import java.util.Arrays; -import java.util.Collections; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Set; - -/** - * The {@link AlgorithmSuite} class is consulted when new OpenPGP keys are being generated to set - * preferred algorithms on the key. - */ -public class AlgorithmSuite { - - private static AlgorithmSuite defaultAlgorithmSuite = new AlgorithmSuite( - Arrays.asList( - SymmetricKeyAlgorithm.AES_256, - SymmetricKeyAlgorithm.AES_192, - SymmetricKeyAlgorithm.AES_128), - Arrays.asList( - HashAlgorithm.SHA512, - HashAlgorithm.SHA384, - HashAlgorithm.SHA256, - HashAlgorithm.SHA224), - Arrays.asList( - CompressionAlgorithm.ZLIB, - CompressionAlgorithm.BZIP2, - CompressionAlgorithm.ZIP, - CompressionAlgorithm.UNCOMPRESSED) - ); - - private final Set symmetricKeyAlgorithms; - private final Set hashAlgorithms; - private final Set compressionAlgorithms; - - public AlgorithmSuite(List symmetricKeyAlgorithms, - List hashAlgorithms, - List compressionAlgorithms) { - this.symmetricKeyAlgorithms = Collections.unmodifiableSet(new LinkedHashSet<>(symmetricKeyAlgorithms)); - this.hashAlgorithms = Collections.unmodifiableSet(new LinkedHashSet<>(hashAlgorithms)); - this.compressionAlgorithms = Collections.unmodifiableSet(new LinkedHashSet<>(compressionAlgorithms)); - } - - public Set getSymmetricKeyAlgorithms() { - return new LinkedHashSet<>(symmetricKeyAlgorithms); - } - - public Set getHashAlgorithms() { - return new LinkedHashSet<>(hashAlgorithms); - } - - public Set getCompressionAlgorithms() { - return new LinkedHashSet<>(compressionAlgorithms); - } - - public static AlgorithmSuite getDefaultAlgorithmSuite() { - return defaultAlgorithmSuite; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/CompressionAlgorithm.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/CompressionAlgorithm.java deleted file mode 100644 index b1f11185..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/CompressionAlgorithm.java +++ /dev/null @@ -1,57 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.algorithm; - -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -import org.bouncycastle.bcpg.CompressionAlgorithmTags; - -/** - * Enumeration of possible compression algorithms. - * - * @see RFC4880: Compression Algorithm Tags - */ -public enum CompressionAlgorithm { - - UNCOMPRESSED (CompressionAlgorithmTags.UNCOMPRESSED), - ZIP (CompressionAlgorithmTags.ZIP), - ZLIB (CompressionAlgorithmTags.ZLIB), - BZIP2 (CompressionAlgorithmTags.BZIP2), - ; - - private static final Map MAP = new ConcurrentHashMap<>(); - - static { - for (CompressionAlgorithm c : CompressionAlgorithm.values()) { - MAP.put(c.algorithmId, c); - } - } - - /** - * Return the {@link CompressionAlgorithm} value that corresponds to the provided numerical id. - * If an invalid id is provided, null is returned. - * - * @param id id - * @return compression algorithm - */ - public static CompressionAlgorithm fromId(int id) { - return MAP.get(id); - } - - private final int algorithmId; - - CompressionAlgorithm(int id) { - this.algorithmId = id; - } - - /** - * Return the numerical algorithm tag corresponding to this compression algorithm. - * @return id - */ - public int getAlgorithmId() { - return algorithmId; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/DocumentSignatureType.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/DocumentSignatureType.java deleted file mode 100644 index 4dbc58da..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/DocumentSignatureType.java +++ /dev/null @@ -1,35 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.algorithm; - -/** - * Subset of {@link SignatureType}, used for signatures over documents. - */ -public enum DocumentSignatureType { - - /** - * Signature is calculated over the unchanged binary data. - */ - BINARY_DOCUMENT(SignatureType.BINARY_DOCUMENT), - - /** - * The signature is calculated over the text data with its line endings converted to - *
-     *     {@code <CR><LF>}
-     * 
. - */ - CANONICAL_TEXT_DOCUMENT(SignatureType.CANONICAL_TEXT_DOCUMENT), - ; - - final SignatureType signatureType; - - DocumentSignatureType(SignatureType signatureType) { - this.signatureType = signatureType; - } - - public SignatureType getSignatureType() { - return signatureType; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/EncryptionPurpose.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/EncryptionPurpose.java deleted file mode 100644 index 1917d837..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/EncryptionPurpose.java +++ /dev/null @@ -1,22 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.algorithm; - -public enum EncryptionPurpose { - /** - * The stream will encrypt communication that goes over the wire. - * Eg. EMail, Chat... - */ - COMMUNICATIONS, - /** - * The stream will encrypt data that is stored on disk. - * Eg. Encrypted backup... - */ - STORAGE, - /** - * The stream will use keys with either flags to encrypt the data. - */ - STORAGE_AND_COMMUNICATIONS -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/Feature.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/Feature.java deleted file mode 100644 index 27837b09..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/Feature.java +++ /dev/null @@ -1,106 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.algorithm; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -import org.bouncycastle.bcpg.sig.Features; - -/** - * An enumeration of features that may be set in the {@link Features} subpacket. - * - * @see RFC4880: Features - */ -public enum Feature { - - /** - * Support for Symmetrically Encrypted Integrity Protected Data Packets using Modification Detection Code Packets. - * - * @see - * RFC-4880 §5.14: Modification Detection Code Packet - */ - MODIFICATION_DETECTION(Features.FEATURE_MODIFICATION_DETECTION), - - /** - * Support for Authenticated Encryption with Additional Data (AEAD). - * If a key announces this feature, it signals support for consuming AEAD Encrypted Data Packets. - * - * NOTE: PGPAINLESS DOES NOT YET SUPPORT THIS FEATURE!!! - * - * @see - * AEAD Encrypted Data Packet - */ - AEAD_ENCRYPTED_DATA(Features.FEATURE_AEAD_ENCRYPTED_DATA), - - /** - * If a key announces this feature, it is a version 5 public key. - * The version 5 format is similar to the version 4 format except for the addition of a count for the key material. - * This count helps parsing secret key packets (which are an extension of the public key packet format) in the case - * of an unknown algorithm. - * In addition, fingerprints of version 5 keys are calculated differently from version 4 keys. - * - * NOTE: PGPAINLESS DOES NOT YET SUPPORT THIS FEATURE!!! - * - * @see - * Public-Key Packet Formats - */ - VERSION_5_PUBLIC_KEY(Features.FEATURE_VERSION_5_PUBLIC_KEY) - ; - - private static final Map MAP = new ConcurrentHashMap<>(); - - static { - for (Feature f : Feature.values()) { - MAP.put(f.featureId, f); - } - } - - public static Feature fromId(byte id) { - return MAP.get(id); - } - - private final byte featureId; - - Feature(byte featureId) { - this.featureId = featureId; - } - - public byte getFeatureId() { - return featureId; - } - - /** - * Convert a bitmask into a list of {@link KeyFlag KeyFlags}. - * - * @param bitmask bitmask - * @return list of key flags encoded by the bitmask - */ - public static List fromBitmask(int bitmask) { - List features = new ArrayList<>(); - for (Feature f : Feature.values()) { - if ((bitmask & f.featureId) != 0) { - features.add(f); - } - } - return features; - } - - /** - * Encode a list of {@link KeyFlag KeyFlags} into a bitmask. - * - * @param features list of flags - * @return bitmask - */ - public static byte toBitmask(Feature... features) { - byte mask = 0; - for (Feature f : features) { - mask |= f.featureId; - } - return mask; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/HashAlgorithm.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/HashAlgorithm.java deleted file mode 100644 index b8c97d7e..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/HashAlgorithm.java +++ /dev/null @@ -1,88 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.algorithm; - -import java.util.HashMap; -import java.util.Map; - -import org.bouncycastle.bcpg.HashAlgorithmTags; - -/** - * An enumeration of different hashing algorithms. - * - * @see RFC4880: Hash Algorithms - */ -public enum HashAlgorithm { - @Deprecated - MD5 (HashAlgorithmTags.MD5, "MD5"), - SHA1 (HashAlgorithmTags.SHA1, "SHA1"), - RIPEMD160 (HashAlgorithmTags.RIPEMD160, "RIPEMD160"), - SHA256 (HashAlgorithmTags.SHA256, "SHA256"), - SHA384 (HashAlgorithmTags.SHA384, "SHA384"), - SHA512 (HashAlgorithmTags.SHA512, "SHA512"), - SHA224 (HashAlgorithmTags.SHA224, "SHA224"), - ; - - private static final Map ID_MAP = new HashMap<>(); - private static final Map NAME_MAP = new HashMap<>(); - - static { - for (HashAlgorithm h : HashAlgorithm.values()) { - ID_MAP.put(h.algorithmId, h); - NAME_MAP.put(h.name, h); - } - } - - /** - * Return the {@link HashAlgorithm} value that corresponds to the provided algorithm id. - * If an invalid algorithm id was provided, null is returned. - * - * @param id numeric id - * @return enum value - */ - public static HashAlgorithm fromId(int id) { - return ID_MAP.get(id); - } - - /** - * Return the {@link HashAlgorithm} value that corresponds to the provided name. - * If an invalid algorithm name was provided, null is returned. - * - * @see RFC4880: §9.4 Hash Algorithms - * for a list of algorithms and names. - * - * @param name text name - * @return enum value - */ - public static HashAlgorithm fromName(String name) { - return NAME_MAP.get(name); - } - - private final int algorithmId; - private final String name; - - HashAlgorithm(int id, String name) { - this.algorithmId = id; - this.name = name; - } - - /** - * Return the numeric algorithm id of the hash algorithm. - * - * @return numeric id - */ - public int getAlgorithmId() { - return algorithmId; - } - - /** - * Return the text name of the hash algorithm. - * - * @return text name - */ - public String getAlgorithmName() { - return name; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/KeyFlag.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/KeyFlag.java deleted file mode 100644 index 69843a93..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/KeyFlag.java +++ /dev/null @@ -1,112 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.algorithm; - -import java.util.ArrayList; -import java.util.List; - -import org.bouncycastle.bcpg.sig.KeyFlags; - -/** - * Enumeration of different key flags. - * Key flags denote different capabilities of a key pair. - * - * @see RFC4880: Key Flags - */ -public enum KeyFlag { - - /** - * This key may be used to certify other keys. - */ - CERTIFY_OTHER (KeyFlags.CERTIFY_OTHER), - - /** - * This key may be used to sign data. - */ - SIGN_DATA (KeyFlags.SIGN_DATA), - - /** - * This key may be used to encrypt communications. - */ - ENCRYPT_COMMS (KeyFlags.ENCRYPT_COMMS), - - /** - * This key may be used to encrypt storage. - */ - ENCRYPT_STORAGE(KeyFlags.ENCRYPT_STORAGE), - - /** - * The private component of this key may have been split by a secret-sharing mechanism. - */ - SPLIT (KeyFlags.SPLIT), - - /** - * This key may be used for authentication. - */ - AUTHENTICATION (KeyFlags.AUTHENTICATION), - - /** - * The private component of this key may be in the possession of more than one person. - */ - SHARED (KeyFlags.SHARED), - ; - - private final int flag; - - KeyFlag(int flag) { - this.flag = flag; - } - - /** - * Return the numeric id of the {@link KeyFlag}. - * - * @return numeric id - */ - public int getFlag() { - return flag; - } - - /** - * Convert a bitmask into a list of {@link KeyFlag KeyFlags}. - * - * @param bitmask bitmask - * @return list of key flags encoded by the bitmask - */ - public static List fromBitmask(int bitmask) { - List flags = new ArrayList<>(); - for (KeyFlag f : KeyFlag.values()) { - if ((bitmask & f.flag) != 0) { - flags.add(f); - } - } - return flags; - } - - /** - * Encode a list of {@link KeyFlag KeyFlags} into a bitmask. - * - * @param flags list of flags - * @return bitmask - */ - public static int toBitmask(KeyFlag... flags) { - int mask = 0; - for (KeyFlag f : flags) { - mask |= f.getFlag(); - } - return mask; - } - - /** - * Return true if the provided bitmask has the bit for the provided flag set. - * Return false if the mask does not contain the flag. - * - * @param mask bitmask - * @param flag flag to be tested for - * @return true if flag is set, false otherwise - */ - public static boolean hasKeyFlag(int mask, KeyFlag flag) { - return (mask & flag.getFlag()) == flag.getFlag(); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/PublicKeyAlgorithm.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/PublicKeyAlgorithm.java deleted file mode 100644 index 2069b2c9..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/PublicKeyAlgorithm.java +++ /dev/null @@ -1,141 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.algorithm; - -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -import org.bouncycastle.bcpg.PublicKeyAlgorithmTags; - -/** - * Enumeration of public key algorithms as defined in RFC4880. - * - * @see RFC4880: Public-Key Algorithms - */ -public enum PublicKeyAlgorithm { - - /** - * RSA capable of encryption and signatures. - */ - RSA_GENERAL (PublicKeyAlgorithmTags.RSA_GENERAL, true, true), - - /** - * RSA with usage encryption. - * - * @deprecated see https://tools.ietf.org/html/rfc4880#section-13.5 - */ - @Deprecated - RSA_ENCRYPT (PublicKeyAlgorithmTags.RSA_ENCRYPT, false, true), - - /** - * RSA with usage of creating signatures. - * - * @deprecated see https://tools.ietf.org/html/rfc4880#section-13.5 - */ - @Deprecated - RSA_SIGN (PublicKeyAlgorithmTags.RSA_SIGN, true, false), - - /** - * ElGamal with usage encryption. - */ - ELGAMAL_ENCRYPT (PublicKeyAlgorithmTags.ELGAMAL_ENCRYPT, false, true), - - /** - * Digital Signature Algorithm. - */ - DSA (PublicKeyAlgorithmTags.DSA, true, false), - - /** - * EC is deprecated. - * @deprecated use {@link #ECDH} instead. - */ - @Deprecated - EC (PublicKeyAlgorithmTags.EC, false, true), - - /** - * Elliptic Curve Diffie-Hellman. - */ - ECDH (PublicKeyAlgorithmTags.ECDH, false, true), - - /** - * Elliptic Curve Digital Signature Algorithm. - */ - ECDSA (PublicKeyAlgorithmTags.ECDSA, true, false), - - /** - * ElGamal General. - * - * @deprecated see https://tools.ietf.org/html/rfc4880#section-13.8 - */ - @Deprecated - ELGAMAL_GENERAL (PublicKeyAlgorithmTags.ELGAMAL_GENERAL, true, true), - - /** - * Diffie-Hellman key exchange algorithm. - */ - DIFFIE_HELLMAN (PublicKeyAlgorithmTags.DIFFIE_HELLMAN, false, true), - - /** - * Digital Signature Algorithm based on twisted Edwards Curves. - */ - EDDSA (PublicKeyAlgorithmTags.EDDSA, true, false), - ; - - private static final Map MAP = new ConcurrentHashMap<>(); - - static { - for (PublicKeyAlgorithm p : PublicKeyAlgorithm.values()) { - MAP.put(p.algorithmId, p); - } - } - - /** - * Return the {@link PublicKeyAlgorithm} that corresponds to the provided algorithm id. - * If an invalid id is provided, null is returned. - * - * @param id numeric algorithm id - * @return algorithm - */ - public static PublicKeyAlgorithm fromId(int id) { - return MAP.get(id); - } - - private final int algorithmId; - private final boolean signingCapable; - private final boolean encryptionCapable; - - PublicKeyAlgorithm(int algorithmId, boolean signingCapable, boolean encryptionCapable) { - this.algorithmId = algorithmId; - this.signingCapable = signingCapable; - this.encryptionCapable = encryptionCapable; - } - - /** - * Return the numeric identifier of the public key algorithm. - * - * @return id - */ - public int getAlgorithmId() { - return algorithmId; - } - - /** - * Return true if this public key algorithm is able to create signatures. - * - * @return true if can sign - */ - public boolean isSigningCapable() { - return signingCapable; - } - - /** - * Return true if this public key algorithm can be used as an encryption algorithm. - * - * @return true if can encrypt - */ - public boolean isEncryptionCapable() { - return encryptionCapable; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/SignatureSubpacket.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/SignatureSubpacket.java deleted file mode 100644 index 96c2e76f..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/SignatureSubpacket.java +++ /dev/null @@ -1,440 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.algorithm; - -import static org.bouncycastle.bcpg.SignatureSubpacketTags.ATTESTED_CERTIFICATIONS; -import static org.bouncycastle.bcpg.SignatureSubpacketTags.CREATION_TIME; -import static org.bouncycastle.bcpg.SignatureSubpacketTags.EMBEDDED_SIGNATURE; -import static org.bouncycastle.bcpg.SignatureSubpacketTags.EXPIRE_TIME; -import static org.bouncycastle.bcpg.SignatureSubpacketTags.EXPORTABLE; -import static org.bouncycastle.bcpg.SignatureSubpacketTags.FEATURES; -import static org.bouncycastle.bcpg.SignatureSubpacketTags.INTENDED_RECIPIENT_FINGERPRINT; -import static org.bouncycastle.bcpg.SignatureSubpacketTags.ISSUER_FINGERPRINT; -import static org.bouncycastle.bcpg.SignatureSubpacketTags.ISSUER_KEY_ID; -import static org.bouncycastle.bcpg.SignatureSubpacketTags.KEY_EXPIRE_TIME; -import static org.bouncycastle.bcpg.SignatureSubpacketTags.KEY_FLAGS; -import static org.bouncycastle.bcpg.SignatureSubpacketTags.KEY_SERVER_PREFS; -import static org.bouncycastle.bcpg.SignatureSubpacketTags.NOTATION_DATA; -import static org.bouncycastle.bcpg.SignatureSubpacketTags.PLACEHOLDER; -import static org.bouncycastle.bcpg.SignatureSubpacketTags.POLICY_URL; -import static org.bouncycastle.bcpg.SignatureSubpacketTags.PREFERRED_AEAD_ALGORITHMS; -import static org.bouncycastle.bcpg.SignatureSubpacketTags.PREFERRED_COMP_ALGS; -import static org.bouncycastle.bcpg.SignatureSubpacketTags.PREFERRED_HASH_ALGS; -import static org.bouncycastle.bcpg.SignatureSubpacketTags.PREFERRED_KEY_SERV; -import static org.bouncycastle.bcpg.SignatureSubpacketTags.PREFERRED_SYM_ALGS; -import static org.bouncycastle.bcpg.SignatureSubpacketTags.PRIMARY_USER_ID; -import static org.bouncycastle.bcpg.SignatureSubpacketTags.REG_EXP; -import static org.bouncycastle.bcpg.SignatureSubpacketTags.REVOCABLE; -import static org.bouncycastle.bcpg.SignatureSubpacketTags.REVOCATION_KEY; -import static org.bouncycastle.bcpg.SignatureSubpacketTags.REVOCATION_REASON; -import static org.bouncycastle.bcpg.SignatureSubpacketTags.SIGNATURE_TARGET; -import static org.bouncycastle.bcpg.SignatureSubpacketTags.SIGNER_USER_ID; -import static org.bouncycastle.bcpg.SignatureSubpacketTags.TRUST_SIG; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -/** - * Enumeration of possible subpackets that might be found in the hashed and unhashed area of an OpenPGP signature. - * - * @see RFC4880: Signature Subpacket Specification - */ -public enum SignatureSubpacket { - - /** - * The time the signature was made. - * MUST be present in the hashed area of the signature. - * - * @see Signature Creation Time - */ - signatureCreationTime(CREATION_TIME), - - /** - * The validity period of the signature. This is the number of seconds - * after the signature creation time that the signature expires. If - * this is not present or has a value of zero, it never expires. - * - * @see Signature Expiration Time - */ - signatureExpirationTime(EXPIRE_TIME), - - /** - * Denotes whether or not the signature is exportable for other users. - * - * @see Exportable Certification - */ - exportableCertification(EXPORTABLE), - - /** - * Signer asserts that the key is not only valid but also trustworthy at - * the specified level. Level 0 has the same meaning as an ordinary - * validity signature. Level 1 means that the signed key is asserted to - * be a valid trusted introducer, with the 2nd octet of the body - * specifying the degree of trust. Level 2 means that the signed key is - * asserted to be trusted to issue level 1 trust signatures, i.e., that - * it is a "meta introducer". Generally, a level n trust signature - * asserts that a key is trusted to issue level n-1 trust signatures. - * The trust amount is in a range from 0-255, interpreted such that - * values less than 120 indicate partial trust and values of 120 or - * greater indicate complete trust. Implementations SHOULD emit values - * of 60 for partial trust and 120 for complete trust. - * - * @see Trust Signature - */ - trustSignature(TRUST_SIG), - - /** - * Used in conjunction with trust Signature packets (of level greater 0) to - * limit the scope of trust that is extended. Only signatures by the - * target key on User IDs that match the regular expression in the body - * of this packet have trust extended by the trust Signature subpacket. - * The regular expression uses the same syntax as the Henry Spencer's - * "almost public domain" regular expression [REGEX] package. A - * description of the syntax is found in Section 8 below. - * - * @see Regular Expression - */ - regularExpression(REG_EXP), - - /** - * Signature's revocability status. The packet body contains a Boolean - * flag indicating whether the signature is revocable. Signatures that - * are not revocable have any later revocation signatures ignored. They - * represent a commitment by the signer that he cannot revoke his - * signature for the life of his key. If this packet is not present, - * the signature is revocable. - * - * @see Revocable - */ - revocable(REVOCABLE), - - /** - * The validity period of the key. This is the number of seconds after - * the key creation time that the key expires. If this is not present - * or has a value of zero, the key never expires. This is found only on - * a self-signature. - * - * @see Key Expiration Time - */ - keyExpirationTime(KEY_EXPIRE_TIME), - - /** - * Placeholder for backwards compatibility. - */ - placeholder(PLACEHOLDER), - - /** - * Symmetric algorithm numbers that indicate which algorithms the key - * holder prefers to use. The subpacket body is an ordered list of - * octets with the most preferred listed first. It is assumed that only - * algorithms listed are supported by the recipient's software. - * This is only found on a self-signature. - * - * @see Preferred Symmetric Algorithms - */ - preferredSymmetricAlgorithms(PREFERRED_SYM_ALGS), - - /** - * Authorizes the specified key to issue revocation signatures for this - * key. Class octet must have bit 0x80 set. If the bit 0x40 is set, - * then this means that the revocation information is sensitive. Other - * bits are for future expansion to other kinds of authorizations. This - * is found on a self-signature. - * - * If the "sensitive" flag is set, the keyholder feels this subpacket - * contains private trust information that describes a real-world - * sensitive relationship. If this flag is set, implementations SHOULD - * NOT export this signature to other users except in cases where the - * data needs to be available: when the signature is being sent to the - * designated revoker, or when it is accompanied by a revocation - * signature from that revoker. Note that it may be appropriate to - * isolate this subpacket within a separate signature so that it is not - * combined with other subpackets that need to be exported. - * - * @see Revocation Key - */ - revocationKey(REVOCATION_KEY), - - /** - * The OpenPGP Key ID of the key issuing the signature. - * - * @see Issuer Key ID - */ - issuerKeyId(ISSUER_KEY_ID), - - /** - * This subpacket describes a "notation" on the signature that the - * issuer wishes to make. The notation has a name and a value, each of - * which are strings of octets. There may be more than one notation in - * a signature. Notations can be used for any extension the issuer of - * the signature cares to make. The "flags" field holds four octets of - * flags. - * - * @see Notation Data - */ - notationData(NOTATION_DATA), - - /** - * Message digest algorithm numbers that indicate which algorithms the - * key holder prefers to receive. Like the preferred symmetric - * algorithms, the list is ordered. - * This is only found on a self-signature. - * - * @see Preferred Hash Algorithms - */ - preferredHashAlgorithms(PREFERRED_HASH_ALGS), - - /** - * Compression algorithm numbers that indicate which algorithms the key - * holder prefers to use. Like the preferred symmetric algorithms, the - * list is ordered. If this subpacket is not included, ZIP is preferred. - * A zero denotes that uncompressed data is preferred; the key holder's - * software might have no compression software in that implementation. - * This is only found on a self-signature. - * - * @see Preferred Compressio Algorithms - */ - preferredCompressionAlgorithms(PREFERRED_COMP_ALGS), - - /** - * This is a list of one-bit flags that indicate preferences that the - * key holder has about how the key is handled on a key server. All - * undefined flags MUST be zero. - * This is found only on a self-signature. - * - * @see Key Server Preferences - */ - keyServerPreferences(KEY_SERVER_PREFS), - - /** - * This is a URI of a key server that the key holder prefers be used for - * updates. Note that keys with multiple User IDs can have a preferred - * key server for each User ID. Note also that since this is a URI, the - * key server can actually be a copy of the key retrieved by ftp, http, - * finger, etc. - * - * @see Preferred Key Server - */ - preferredKeyServers(PREFERRED_KEY_SERV), - - /** - * This is a flag in a User ID's self-signature that states whether this - * User ID is the main User ID for this key. It is reasonable for an - * implementation to resolve ambiguities in preferences, etc. by - * referring to the primary User ID. If this flag is absent, its value - * is zero. If more than one User ID in a key is marked as primary, the - * implementation may resolve the ambiguity in any way it sees fit, but - * it is RECOMMENDED that priority be given to the User ID with the most - * recent self-signature. - * - * When appearing on a self-signature on a User ID packet, this - * subpacket applies only to User ID packets. When appearing on a - * self-signature on a User Attribute packet, this subpacket applies - * only to User Attribute packets. That is to say, there are two - * different and independent "primaries" -- one for User IDs, and one - * for User Attributes. - * - * @see Primary User-ID - */ - primaryUserId(PRIMARY_USER_ID), - - /** - * This subpacket contains a URI of a document that describes the policy - * under which the signature was issued. - * - * @see Policy URL - */ - policyUrl(POLICY_URL), - - /** - * This subpacket contains a list of binary flags that hold information - * about a key. It is a string of octets, and an implementation MUST - * NOT assume a fixed size. This is so it can grow over time. If a - * list is shorter than an implementation expects, the unstated flags - * are considered to be zero. - * - * @see Key Flags - */ - keyFlags(KEY_FLAGS), - - /** - * This subpacket allows a keyholder to state which User ID is - * responsible for the signing. Many keyholders use a single key for - * different purposes, such as business communications as well as - * personal communications. This subpacket allows such a keyholder to - * state which of their roles is making a signature. - * - * @see Signer's User ID - */ - signerUserId(SIGNER_USER_ID), - - /** - * This subpacket is used only in key revocation and certification - * revocation signatures. It describes the reason why the key or - * certificate was revoked. - * - * The first octet contains a machine-readable code that denotes the - * reason for the revocation: - * - * 0 - No reason specified (key revocations or cert revocations) - * 1 - Key is superseded (key revocations) - * 2 - Key material has been compromised (key revocations) - * 3 - Key is retired and no longer used (key revocations) - * 32 - User ID information is no longer valid (cert revocations) - * 100-110 - Private Use - * - * @see Reason for Revocation - */ - revocationReason(REVOCATION_REASON), - - /** - * The Features subpacket denotes which advanced OpenPGP features a - * user's implementation supports. This is so that as features are - * added to OpenPGP that cannot be backwards-compatible, a user can - * state that they can use that feature. The flags are single bits that - * indicate that a given feature is supported. - * - * This subpacket is similar to a preferences subpacket, and only - * appears in a self-signature. - * - * @see Features - */ - features(FEATURES), - - /** - * This subpacket identifies a specific target signature to which a - * signature refers. For revocation signatures, this subpacket - * provides explicit designation of which signature is being revoked. - * For a third-party or timestamp signature, this designates what - * signature is signed. All arguments are an identifier of that target - * signature. - * - * The N octets of hash data MUST be the size of the hash of the - * signature. For example, a target signature with a SHA-1 hash MUST - * have 20 octets of hash data. - * - * @see Signature Target - */ - signatureTarget(SIGNATURE_TARGET), - - /** - * This subpacket contains a complete Signature packet body as - * specified in Section 5.2 above. It is useful when one signature - * needs to refer to, or be incorporated in, another signature. - * - * @see Embedded Signature - */ - embeddedSignature(EMBEDDED_SIGNATURE), - - /** - * The OpenPGP Key fingerprint of the key issuing the signature. This - * subpacket SHOULD be included in all signatures. If the version of - * the issuing key is 4 and an Issuer subpacket is also included in the - * signature, the key ID of the Issuer subpacket MUST match the low 64 - * bits of the fingerprint. - * - * Note that the length N of the fingerprint for a version 4 key is 20 - * octets; for a version 5 key N is 32. - * - * @see Issuer Fingerprint - */ - issuerFingerprint(ISSUER_FINGERPRINT), - - /** - * AEAD algorithm numbers that indicate which AEAD algorithms the key - * holder prefers to use. The subpacket body is an ordered list of - * octets with the most preferred listed first. It is assumed that only - * algorithms listed are supported by the recipient's software. - * This is only found on a self-signature. - * Note that support for the AEAD Encrypted Data packet in the general - * is indicated by a Feature Flag. - * - * @see Preferred AEAD Algorithms - */ - preferredAEADAlgorithms(PREFERRED_AEAD_ALGORITHMS), - - /** - * The OpenPGP Key fingerprint of the intended recipient primary key. - * If one or more subpackets of this type are included in a signature, - * it SHOULD be considered valid only in an encrypted context, where the - * key it was encrypted to is one of the indicated primary keys, or one - * of their subkeys. This can be used to prevent forwarding a signature - * outside of its intended, encrypted context. - * - * Note that the length N of the fingerprint for a version 4 key is 20 - * octets; for a version 5 key N is 32. - * - * @see Intended Recipient Fingerprint - */ - intendedRecipientFingerprint(INTENDED_RECIPIENT_FINGERPRINT), - - /** - * This subpacket MUST only appear as a hashed subpacket of an - * Attestation Key Signature. It has no meaning in any other signature - * type. It is used by the primary key to attest to a set of third- - * party certifications over the associated User ID or User Attribute. - * This enables the holder of an OpenPGP primary key to mark specific - * third-party certifications as re-distributable with the rest of the - * Transferable Public Key (see the "No-modify" flag in "Key Server - * Preferences", above). Implementations MUST include exactly one - * Attested Certification subpacket in any generated Attestation Key - * Signature. - * - * @see Attested Certification - */ - attestedCertification(ATTESTED_CERTIFICATIONS) - ; - - private static final Map MAP = new ConcurrentHashMap<>(); - static { - for (SignatureSubpacket p : values()) { - MAP.put(p.code, p); - } - } - - private final int code; - - SignatureSubpacket(int code) { - this.code = code; - } - - /** - * Return the numerical identifier of the {@link SignatureSubpacket}. - * @return id - */ - public int getCode() { - return code; - } - - /** - * Return the {@link SignatureSubpacket} that corresponds to the provided id. - * - * @param code id - * @return signature subpacket - */ - public static SignatureSubpacket fromCode(int code) { - SignatureSubpacket tag = MAP.get(code); - if (tag == null) { - throw new IllegalArgumentException("No SignatureSubpacket tag found with code " + code); - } - return tag; - } - - /** - * Convert an array of signature subpacket tags into a list of {@link SignatureSubpacket SignatureSubpackets}. - * - * @param codes array of codes - * @return list of subpackets - */ - public static List fromCodes(int[] codes) { - List tags = new ArrayList<>(); - for (int code : codes) { - tags.add(fromCode(code)); - } - return tags; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/SignatureType.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/SignatureType.java deleted file mode 100644 index a31dd31a..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/SignatureType.java +++ /dev/null @@ -1,222 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.algorithm; - -import org.bouncycastle.openpgp.PGPSignature; - -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -/** - * Enum that enlists all the Signature Types defined in rfc4880 section 5.2.1 - * See {@link org.bouncycastle.openpgp.PGPSignature} for comparison. - * - * @see rfc4880 §5.2.1. Signature Types - */ -public enum SignatureType { - - /** - * Signature of a binary document. - * This means the signer owns it, created it, or certifies that it - * has not been modified. - */ - BINARY_DOCUMENT(PGPSignature.BINARY_DOCUMENT), - - /** - * Signature of a canonical text document. - * This means the signer owns it, created it, or certifies that it - * has not been modified. The signature is calculated over the text - * data with its line endings converted to {@code }. - */ - CANONICAL_TEXT_DOCUMENT(PGPSignature.CANONICAL_TEXT_DOCUMENT), - - /** - * Standalone signature. - * This signature is a signature of only its own subpacket contents. - * It is calculated identically to a signature over a zero-length - * binary document. Note that it doesn't make sense to have a V3 - * standalone signature. - */ - STANDALONE(PGPSignature.STAND_ALONE), - - /** - * Generic certification of a User ID and Public-Key packet. - * The issuer of this certification does not make any particular - * assertion as to how well the certifier has checked that the owner - * of the key is in fact the person described by the User ID. - */ - GENERIC_CERTIFICATION(PGPSignature.DEFAULT_CERTIFICATION), - - /** - * Persona certification of a User ID and Public-Key packet. - * The issuer of this certification has not done any verification of - * the claim that the owner of this key is the User ID specified. - */ - NO_CERTIFICATION(PGPSignature.NO_CERTIFICATION), - - /** - * Casual certification of a User ID and Public-Key packet. - * The issuer of this certification has done some casual - * verification of the claim of identity. - */ - CASUAL_CERTIFICATION(PGPSignature.CASUAL_CERTIFICATION), - - /** - * Positive certification of a User ID and Public-Key packet. - * The issuer of this certification has done substantial - * verification of the claim of identity. - */ - POSITIVE_CERTIFICATION(PGPSignature.POSITIVE_CERTIFICATION), - - /** - * Subkey Binding Signature. - * This signature is a statement by the top-level signing key that - * indicates that it owns the subkey. This signature is calculated - * directly on the primary key and subkey, and not on any User ID or - * other packets. A signature that binds a signing subkey MUST have - * an Embedded Signature subpacket in this binding signature that - * contains a {@link #PRIMARYKEY_BINDING} signature made by the - * signing subkey on the primary key and subkey. - */ - SUBKEY_BINDING(PGPSignature.SUBKEY_BINDING), - - /** - * Primary Key Binding Signature - * This signature is a statement by a signing subkey, indicating - * that it is owned by the primary key and subkey. This signature - * is calculated the same way as a {@link #SUBKEY_BINDING} signature: - * directly on the primary key and subkey, and not on any User ID or - * other packets. - */ - PRIMARYKEY_BINDING(PGPSignature.PRIMARYKEY_BINDING), - - /** - * Signature directly on a key - * This signature is calculated directly on a key. It binds the - * information in the Signature subpackets to the key, and is - * appropriate to be used for subpackets that provide information - * about the key, such as the Revocation Key subpacket. It is also - * appropriate for statements that non-self certifiers want to make - * about the key itself, rather than the binding between a key and a - * name. - */ - DIRECT_KEY(PGPSignature.DIRECT_KEY), - - /** - * Key revocation signature - * The signature is calculated directly on the key being revoked. A - * revoked key is not to be used. Only revocation signatures by the - * key being revoked, or by an authorized revocation key, should be - * considered valid revocation signatures. - */ - KEY_REVOCATION(PGPSignature.KEY_REVOCATION), - - /** - * Subkey revocation signature - * The signature is calculated directly on the subkey being revoked. - * A revoked subkey is not to be used. Only revocation signatures - * by the top-level signature key that is bound to this subkey, or - * by an authorized revocation key, should be considered valid - * revocation signatures. - */ - SUBKEY_REVOCATION(PGPSignature.SUBKEY_REVOCATION), - - /** - * Certification revocation signature - * This signature revokes an earlier User ID certification signature - * (signature class 0x10 through 0x13) or signature {@link #DIRECT_KEY}. - * It should be issued by the same key that issued the - * revoked signature or an authorized revocation key. The signature - * is computed over the same data as the certificate that it - * revokes, and should have a later creation date than that - * certificate. - */ - CERTIFICATION_REVOCATION(PGPSignature.CERTIFICATION_REVOCATION), - - /** - * Timestamp signature. - * This signature is only meaningful for the timestamp contained in - * it. - */ - TIMESTAMP(PGPSignature.TIMESTAMP), - - /** - * Third-Party Confirmation signature. - * This signature is a signature over some other OpenPGP Signature - * packet(s). It is analogous to a notary seal on the signed data. - * A third-party signature SHOULD include Signature Target - * subpacket(s) to give easy identification. Note that we really do - * mean SHOULD. There are plausible uses for this (such as a blind - * party that only sees the signature, not the key or source - * document) that cannot include a target subpacket. - */ - THIRD_PARTY_CONFIRMATION(0x50) - ; - - private static final Map map = new ConcurrentHashMap<>(); - static { - for (SignatureType sigType : SignatureType.values()) { - map.put(sigType.getCode(), sigType); - } - } - - /** - * Convert a numerical id into a {@link SignatureType}. - * - * @param code numeric id - * @return signature type enum - */ - public static SignatureType valueOf(int code) { - SignatureType type = map.get(code); - if (type != null) { - return type; - } - throw new IllegalArgumentException("Signature type 0x" + Integer.toHexString(code) + " appears to be invalid."); - } - - private final int code; - - SignatureType(int code) { - this.code = code; - } - - /** - * Return the numeric id of the signature type enum. - * - * @return numeric id - */ - public int getCode() { - return code; - } - - public static boolean isRevocationSignature(int signatureType) { - return isRevocationSignature(SignatureType.valueOf(signatureType)); - } - - public static boolean isRevocationSignature(SignatureType signatureType) { - switch (signatureType) { - case BINARY_DOCUMENT: - case CANONICAL_TEXT_DOCUMENT: - case STANDALONE: - case GENERIC_CERTIFICATION: - case NO_CERTIFICATION: - case CASUAL_CERTIFICATION: - case POSITIVE_CERTIFICATION: - case SUBKEY_BINDING: - case PRIMARYKEY_BINDING: - case DIRECT_KEY: - case TIMESTAMP: - case THIRD_PARTY_CONFIRMATION: - return false; - case KEY_REVOCATION: - case SUBKEY_REVOCATION: - case CERTIFICATION_REVOCATION: - return true; - default: - throw new IllegalArgumentException("Unknown signature type: " + signatureType); - } - } - -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/StreamEncoding.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/StreamEncoding.java deleted file mode 100644 index 3ea9507b..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/StreamEncoding.java +++ /dev/null @@ -1,77 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.algorithm; - -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -import org.bouncycastle.openpgp.PGPLiteralData; - -/** - * Enumeration of possible encoding formats of the content of the literal data packet. - * - * @see RFC4880: Literal Data Packet - */ -public enum StreamEncoding { - - /** - * The Literal packet contains binary data. - */ - BINARY(PGPLiteralData.BINARY), - - /** - * The Literal packet contains text data, and thus may need line ends converted to local form, or other - * text-mode changes. - */ - TEXT(PGPLiteralData.TEXT), - - /** - * Indication that the implementation believes that the literal data contains UTF-8 text. - */ - UTF8(PGPLiteralData.UTF8), - - /** - * Early versions of PGP also defined a value of 'l' as a 'local' mode for machine-local conversions. - * RFC 1991 [RFC1991] incorrectly stated this local mode flag as '1' (ASCII numeral one). - * Both of these local modes are deprecated. - */ - @Deprecated - LOCAL('l'), - ; - - private final char code; - - private static final Map MAP = new ConcurrentHashMap<>(); - static { - for (StreamEncoding f : StreamEncoding.values()) { - MAP.put(f.code, f); - } - // RFC 1991 [RFC1991] incorrectly stated local mode flag as '1', see doc of LOCAL. - MAP.put('1', LOCAL); - } - - StreamEncoding(char code) { - this.code = code; - } - - /** - * Return the code identifier of the encoding. - * - * @return identifier - */ - public char getCode() { - return code; - } - - /** - * Return the {@link StreamEncoding} corresponding to the provided code identifier. - * - * @param code identifier - * @return encoding enum - */ - public static StreamEncoding fromCode(int code) { - return MAP.get((char) code); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/SymmetricKeyAlgorithm.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/SymmetricKeyAlgorithm.java deleted file mode 100644 index 43c327cc..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/SymmetricKeyAlgorithm.java +++ /dev/null @@ -1,127 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.algorithm; - -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -import org.bouncycastle.bcpg.SymmetricKeyAlgorithmTags; - -/** - * Enumeration of possible symmetric encryption algorithms. - * - * @see RFC4880: Symmetric-Key Algorithms - */ -public enum SymmetricKeyAlgorithm { - - /** - * Plaintext or unencrypted data. - */ - NULL (SymmetricKeyAlgorithmTags.NULL), - - /** - * IDEA is deprecated. - * @deprecated use a different algorithm. - */ - @Deprecated - IDEA (SymmetricKeyAlgorithmTags.IDEA), - - /** - * TripleDES (DES-EDE - 168 bit key derived from 192). - */ - TRIPLE_DES (SymmetricKeyAlgorithmTags.TRIPLE_DES), - - /** - * CAST5 (128 bit key, as per RFC2144). - */ - CAST5 (SymmetricKeyAlgorithmTags.CAST5), - - /** - * Blowfish (128 bit key, 16 rounds). - */ - BLOWFISH (SymmetricKeyAlgorithmTags.BLOWFISH), - - /** - * Reserved in RFC4880. - * SAFER-SK128 (13 rounds) - */ - SAFER (SymmetricKeyAlgorithmTags.SAFER), - - /** - * Reserved in RFC4880. - * Reserved for DES/SK - */ - DES (SymmetricKeyAlgorithmTags.DES), - - /** - * AES with 128-bit key. - */ - AES_128 (SymmetricKeyAlgorithmTags.AES_128), - - /** - * AES with 192-bit key. - */ - AES_192 (SymmetricKeyAlgorithmTags.AES_192), - - /** - * AES with 256-bit key. - */ - AES_256 (SymmetricKeyAlgorithmTags.AES_256), - - /** - * Twofish with 256-bit key. - */ - TWOFISH (SymmetricKeyAlgorithmTags.TWOFISH), - - /** - * Reserved for Camellia with 128-bit key. - */ - CAMELLIA_128 (SymmetricKeyAlgorithmTags.CAMELLIA_128), - - /** - * Reserved for Camellia with 192-bit key. - */ - CAMELLIA_192 (SymmetricKeyAlgorithmTags.CAMELLIA_192), - - /** - * Reserved for Camellia with 256-bit key. - */ - CAMELLIA_256 (SymmetricKeyAlgorithmTags.CAMELLIA_256), - ; - - private static final Map MAP = new ConcurrentHashMap<>(); - - static { - for (SymmetricKeyAlgorithm s : SymmetricKeyAlgorithm.values()) { - MAP.put(s.algorithmId, s); - } - } - - /** - * Return the {@link SymmetricKeyAlgorithm} enum that corresponds to the provided numeric id. - * If an invalid id is provided, null is returned. - * - * @param id numeric algorithm id - * @return symmetric key algorithm enum - */ - public static SymmetricKeyAlgorithm fromId(int id) { - return MAP.get(id); - } - - private final int algorithmId; - - SymmetricKeyAlgorithm(int algorithmId) { - this.algorithmId = algorithmId; - } - - /** - * Return the numeric algorithm id of the enum. - * - * @return numeric id - */ - public int getAlgorithmId() { - return algorithmId; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/negotiation/SymmetricKeyAlgorithmNegotiator.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/negotiation/SymmetricKeyAlgorithmNegotiator.java deleted file mode 100644 index 427bcc69..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/negotiation/SymmetricKeyAlgorithmNegotiator.java +++ /dev/null @@ -1,88 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.algorithm.negotiation; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; -import org.pgpainless.policy.Policy; - -/** - * Interface for symmetric key algorithm negotiation. - */ -public interface SymmetricKeyAlgorithmNegotiator { - - /** - * Negotiate a symmetric encryption algorithm. - * - * @param policy algorithm policy - * @param override algorithm override (if not null, return this) - * @param keyPreferences list of preferences per key - * @return negotiated algorithm - */ - SymmetricKeyAlgorithm negotiate(Policy.SymmetricKeyAlgorithmPolicy policy, SymmetricKeyAlgorithm override, List> keyPreferences); - - static SymmetricKeyAlgorithmNegotiator byPopularity() { - return new SymmetricKeyAlgorithmNegotiator() { - @Override - public SymmetricKeyAlgorithm negotiate(Policy.SymmetricKeyAlgorithmPolicy policy, SymmetricKeyAlgorithm override, List> preferences) { - if (override == SymmetricKeyAlgorithm.NULL) { - throw new IllegalArgumentException("Algorithm override cannot be NULL (plaintext)."); - } - - if (override != null) { - return override; - } - - // Count score (occurrences) of each algorithm - Map supportWeight = new LinkedHashMap<>(); - for (Set keyPreferences : preferences) { - for (SymmetricKeyAlgorithm preferred : keyPreferences) { - if (supportWeight.containsKey(preferred)) { - supportWeight.put(preferred, supportWeight.get(preferred) + 1); - } else { - supportWeight.put(preferred, 1); - } - } - } - - // Pivot the score map - Map> byScore = new HashMap<>(); - for (SymmetricKeyAlgorithm algorithm : supportWeight.keySet()) { - int score = supportWeight.get(algorithm); - List withSameScore = byScore.get(score); - if (withSameScore == null) { - withSameScore = new ArrayList<>(); - byScore.put(score, withSameScore); - } - withSameScore.add(algorithm); - } - - List scores = new ArrayList<>(byScore.keySet()); - - // Sort map and iterate from highest to lowest score - Collections.sort(scores); - for (int i = scores.size() - 1; i >= 0; i--) { - int score = scores.get(i); - List withSameScore = byScore.get(score); - // Select best algorithm - SymmetricKeyAlgorithm best = policy.selectBest(withSameScore); - if (best != null) { - return best; - } - } - - // If no algorithm is acceptable, choose fallback - return policy.getDefaultSymmetricKeyAlgorithm(); - } - }; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/negotiation/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/negotiation/package-info.java deleted file mode 100644 index 3969d8df..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/negotiation/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Classes related to algorithm negotiation. - */ -package org.pgpainless.algorithm.negotiation; diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/package-info.java deleted file mode 100644 index c2f5f5ff..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Enums which map to OpenPGP's algorithm IDs. - */ -package org.pgpainless.algorithm; diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/CloseForResultInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/CloseForResultInputStream.java deleted file mode 100644 index ba895a08..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/CloseForResultInputStream.java +++ /dev/null @@ -1,39 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification; - -import java.io.IOException; -import java.io.InputStream; -import javax.annotation.Nonnull; - -public abstract class CloseForResultInputStream extends InputStream { - - protected final OpenPgpMetadata.Builder resultBuilder; - private boolean isClosed = false; - - public CloseForResultInputStream(@Nonnull OpenPgpMetadata.Builder resultBuilder) { - this.resultBuilder = resultBuilder; - } - - @Override - public void close() throws IOException { - this.isClosed = true; - } - - /** - * Return the result of the decryption. - * The result contains metadata about the decryption, such as signatures, used keys and algorithms, as well as information - * about the decrypted file/stream. - * - * Can only be obtained once the stream got successfully closed ({@link #close()}). - * @return metadata - */ - public OpenPgpMetadata getResult() { - if (!isClosed) { - throw new IllegalStateException("Stream MUST be closed before the result can be accessed."); - } - return resultBuilder.build(); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java deleted file mode 100644 index 3c1a9454..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java +++ /dev/null @@ -1,330 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification; - -import java.io.IOException; -import java.io.InputStream; -import java.util.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; -import org.bouncycastle.openpgp.PGPSignature; -import org.pgpainless.exception.NotYetImplementedException; -import org.pgpainless.key.protection.SecretKeyRingProtector; -import org.pgpainless.signature.SignatureUtils; -import org.pgpainless.util.Passphrase; - -/** - * Options for decryption and signature verification. - */ -public class ConsumerOptions { - - - private boolean ignoreMDCErrors = false; - - private Date verifyNotBefore = null; - private Date verifyNotAfter = new Date(); - - // Set of verification keys - private final Set certificates = new HashSet<>(); - private final Set detachedSignatures = new HashSet<>(); - private MissingPublicKeyCallback missingCertificateCallback = null; - - // Session key for decryption without passphrase/key - private byte[] sessionKey = null; - - private final Map decryptionKeys = new HashMap<>(); - private final Set decryptionPassphrases = new HashSet<>(); - private MissingKeyPassphraseStrategy missingKeyPassphraseStrategy = MissingKeyPassphraseStrategy.INTERACTIVE; - - - /** - * Consider signatures on the message made before the given timestamp invalid. - * Null means no limitation. - * - * @param timestamp timestamp - * @return options - */ - public ConsumerOptions verifyNotBefore(Date timestamp) { - this.verifyNotBefore = timestamp; - return this; - } - - /** - * Return the earliest creation date on which signatures on the message are considered valid. - * Signatures made earlier than this date are considered invalid. - * - * @return earliest allowed signature creation date or null - */ - public @Nullable Date getVerifyNotBefore() { - return verifyNotBefore; - } - - /** - * Consider signatures on the message made after the given timestamp invalid. - * Null means no limitation. - * - * @param timestamp timestamp - * @return options - */ - public ConsumerOptions verifyNotAfter(Date timestamp) { - this.verifyNotAfter = timestamp; - return this; - } - - /** - * Return the latest possible creation date on which signatures made on the message are considered valid. - * Signatures made later than this date are considered invalid. - * - * @return Latest possible creation date or null. - */ - public Date getVerifyNotAfter() { - return verifyNotAfter; - } - - /** - * Add a certificate (public key ring) for signature verification. - * - * @param verificationCert certificate for signature verification - * @return options - */ - public ConsumerOptions addVerificationCert(PGPPublicKeyRing verificationCert) { - this.certificates.add(verificationCert); - return this; - } - - /** - * Add a set of certificates (public key rings) for signature verification. - * - * @param verificationCerts certificates for signature verification - * @return options - */ - public ConsumerOptions addVerificationCerts(PGPPublicKeyRingCollection verificationCerts) { - for (PGPPublicKeyRing certificate : verificationCerts) { - addVerificationCert(certificate); - } - return this; - } - - public ConsumerOptions addVerificationOfDetachedSignatures(InputStream signatureInputStream) throws IOException, PGPException { - List signatures = SignatureUtils.readSignatures(signatureInputStream); - return addVerificationOfDetachedSignatures(signatures); - } - - public ConsumerOptions addVerificationOfDetachedSignatures(List detachedSignatures) { - for (PGPSignature signature : detachedSignatures) { - addVerificationOfDetachedSignature(signature); - } - return this; - } - - /** - * Add a detached signature for the signature verification process. - * - * @param detachedSignature detached signature - * @return options - */ - public ConsumerOptions addVerificationOfDetachedSignature(PGPSignature detachedSignature) { - detachedSignatures.add(detachedSignature); - return this; - } - - /** - * Set a callback that's used when a certificate (public key) is missing for signature verification. - * - * @param callback callback - * @return options - */ - public ConsumerOptions setMissingCertificateCallback(MissingPublicKeyCallback callback) { - this.missingCertificateCallback = callback; - return this; - } - - - /** - * Attempt decryption using a session key. - * - * Note: PGPainless does not yet support decryption with session keys. - * TODO: Add support for decryption using session key. - * - * @see RFC4880 on Session Keys - * - * @param sessionKey session key - * @return options - */ - public ConsumerOptions setSessionKey(@Nonnull byte[] sessionKey) { - this.sessionKey = sessionKey; - throw new NotYetImplementedException(); - } - - /** - * Return the session key. - * - * @return session key or null - */ - public @Nullable byte[] getSessionKey() { - if (sessionKey == null) { - return null; - } - - byte[] sk = new byte[sessionKey.length]; - System.arraycopy(sessionKey, 0, sk, 0, sessionKey.length); - return sk; - } - - /** - * Add a key for message decryption. - * The key is expected to be unencrypted. - * - * @param key unencrypted key - * @return options - */ - public ConsumerOptions addDecryptionKey(@Nonnull PGPSecretKeyRing key) { - return addDecryptionKey(key, SecretKeyRingProtector.unprotectedKeys()); - } - - /** - * Add a key for message decryption. If the key is encrypted, the {@link SecretKeyRingProtector} is used to decrypt it - * when needed. - * - * @param key key - * @param keyRingProtector protector for the secret key - * @return options - */ - public ConsumerOptions addDecryptionKey(@Nonnull PGPSecretKeyRing key, @Nonnull SecretKeyRingProtector keyRingProtector) { - decryptionKeys.put(key, keyRingProtector); - return this; - } - - /** - * Add the keys in the provided key collection for message decryption. - * - * @param keys key collection - * @param keyRingProtector protector for encrypted secret keys - * @return options - */ - public ConsumerOptions addDecryptionKeys(@Nonnull PGPSecretKeyRingCollection keys, @Nonnull SecretKeyRingProtector keyRingProtector) { - for (PGPSecretKeyRing key : keys) { - addDecryptionKey(key, keyRingProtector); - } - return this; - } - - /** - * Add a passphrase for message decryption. - * This passphrase will be used to try to decrypt messages which were symmetrically encrypted for a passphrase. - * - * @see Symmetrically Encrypted Data Packet - * - * @param passphrase passphrase - * @return options - */ - public ConsumerOptions addDecryptionPassphrase(@Nonnull Passphrase passphrase) { - decryptionPassphrases.add(passphrase); - return this; - } - - public @Nonnull Set getDecryptionKeys() { - return Collections.unmodifiableSet(decryptionKeys.keySet()); - } - - public @Nonnull Set getDecryptionPassphrases() { - return Collections.unmodifiableSet(decryptionPassphrases); - } - - public @Nonnull Set getCertificates() { - return Collections.unmodifiableSet(certificates); - } - - public @Nullable MissingPublicKeyCallback getMissingCertificateCallback() { - return missingCertificateCallback; - } - - public @Nonnull SecretKeyRingProtector getSecretKeyProtector(PGPSecretKeyRing decryptionKeyRing) { - return decryptionKeys.get(decryptionKeyRing); - } - - public @Nonnull Set getDetachedSignatures() { - return Collections.unmodifiableSet(detachedSignatures); - } - - /** - * By default, PGPainless will require encrypted messages to make use of SEIP data packets. - * Those are Symmetrically Encrypted Integrity Protected Data packets. - * Symmetrically Encrypted Data Packets without integrity protection are rejected by default. - * Furthermore, PGPainless will throw an exception if verification of the MDC error detection code of the SEIP packet - * fails. - * - * Failure of MDC verification indicates a tampered ciphertext, which might be the cause of an attack or data corruption. - * - * This method can be used to ignore MDC errors and allow PGPainless to consume encrypted data without integrity protection. - * If the flag
ignoreMDCErrors
is set to true, PGPainless will - *
    - *
  • not throw exceptions for SEIP packets with tampered ciphertext
  • - *
  • not throw exceptions for SEIP packets with tampered MDC
  • - *
  • not throw exceptions for MDCs with bad CTB
  • - *
  • not throw exceptions for MDCs with bad length
  • - *
- * - * It will however still throw an exception if it encounters a SEIP packet with missing or truncated MDC - * - * @see Sym. Encrypted Integrity Protected Data Packet - * @param ignoreMDCErrors true if MDC errors or missing MDCs shall be ignored, false otherwise. - * @return options - */ - @Deprecated - public ConsumerOptions setIgnoreMDCErrors(boolean ignoreMDCErrors) { - this.ignoreMDCErrors = ignoreMDCErrors; - return this; - } - - /** - * Return true, if PGPainless is ignoring MDC errors. - * - * @return ignore mdc errors - */ - boolean isIgnoreMDCErrors() { - return ignoreMDCErrors; - } - - /** - * Specify the {@link MissingKeyPassphraseStrategy}. - * This strategy defines, how missing passphrases for unlocking secret keys are handled. - * In interactive mode ({@link MissingKeyPassphraseStrategy#INTERACTIVE}) PGPainless will try to obtain missing - * passphrases for secret keys via the {@link SecretKeyRingProtector SecretKeyRingProtectors} - * {@link org.pgpainless.key.protection.passphrase_provider.SecretKeyPassphraseProvider} callback. - * - * In non-interactice mode ({@link MissingKeyPassphraseStrategy#THROW_EXCEPTION}, PGPainless will instead - * throw a {@link org.pgpainless.exception.MissingPassphraseException} containing the ids of all keys for which - * there are missing passphrases. - * - * @param strategy strategy - * @return options - */ - public ConsumerOptions setMissingKeyPassphraseStrategy(MissingKeyPassphraseStrategy strategy) { - this.missingKeyPassphraseStrategy = strategy; - return this; - } - - /** - * Return the currently configured {@link MissingKeyPassphraseStrategy}. - * - * @return missing key passphrase strategy - */ - MissingKeyPassphraseStrategy getMissingKeyPassphraseStrategy() { - return missingKeyPassphraseStrategy; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilder.java deleted file mode 100644 index 6dcc355b..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilder.java +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification; - -import java.io.IOException; -import java.io.InputStream; -import javax.annotation.Nonnull; - -import org.bouncycastle.openpgp.PGPException; - -public class DecryptionBuilder implements DecryptionBuilderInterface { - - private InputStream inputStream; - - @Override - public DecryptWith onInputStream(@Nonnull InputStream inputStream) { - this.inputStream = inputStream; - return new DecryptWithImpl(); - } - - class DecryptWithImpl implements DecryptWith { - - @Override - public DecryptionStream withOptions(ConsumerOptions consumerOptions) throws PGPException, IOException { - if (consumerOptions == null) { - throw new IllegalArgumentException("Consumer options cannot be null."); - } - - return DecryptionStreamFactory.create(inputStream, consumerOptions); - } - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilderInterface.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilderInterface.java deleted file mode 100644 index 07db42f0..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilderInterface.java +++ /dev/null @@ -1,36 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification; - -import java.io.IOException; -import java.io.InputStream; -import javax.annotation.Nonnull; - -import org.bouncycastle.openpgp.PGPException; - -public interface DecryptionBuilderInterface { - - /** - * Create a {@link DecryptionStream} on an {@link InputStream} which contains the encrypted and/or signed data. - * - * @param inputStream encrypted and/or signed data. - * @return api handle - */ - DecryptWith onInputStream(@Nonnull InputStream inputStream); - - interface DecryptWith { - - /** - * Add options for decryption / signature verification, such as keys, passphrases etc. - * - * @param consumerOptions consumer options - * @return decryption stream - * @throws PGPException in case of an OpenPGP related error - * @throws IOException in case of an IO error - */ - DecryptionStream withOptions(ConsumerOptions consumerOptions) throws PGPException, IOException; - - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStream.java deleted file mode 100644 index e3f1e720..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStream.java +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification; - -import java.io.IOException; -import java.io.InputStream; -import javax.annotation.Nonnull; - -import org.bouncycastle.util.io.Streams; - -/** - * Decryption Stream that handles updating and verification of detached signatures, - * as well as verification of integrity-protected input streams once the stream gets closed. - */ -public class DecryptionStream extends CloseForResultInputStream { - - private final InputStream inputStream; - private final IntegrityProtectedInputStream integrityProtectedInputStream; - private final InputStream armorStream; - - /** - * Create an input stream that handles decryption and - if necessary - integrity protection verification. - * - * @param wrapped underlying input stream - * @param resultBuilder builder for decryption metadata like algorithms, recipients etc. - * @param integrityProtectedInputStream in case of data encrypted using SEIP packet close this stream to check integrity - * @param armorStream armor stream to verify CRC checksums - */ - DecryptionStream(@Nonnull InputStream wrapped, - @Nonnull OpenPgpMetadata.Builder resultBuilder, - IntegrityProtectedInputStream integrityProtectedInputStream, - InputStream armorStream) { - super(resultBuilder); - this.inputStream = wrapped; - this.integrityProtectedInputStream = integrityProtectedInputStream; - this.armorStream = armorStream; - } - - @Override - public void close() throws IOException { - if (armorStream != null) { - Streams.drain(armorStream); - } - inputStream.close(); - if (integrityProtectedInputStream != null) { - integrityProtectedInputStream.close(); - } - super.close(); - } - - @Override - public int read() throws IOException { - int r = inputStream.read(); - return r; - } - - @Override - public int read(@Nonnull byte[] bytes, int offset, int length) throws IOException { - int read = inputStream.read(bytes, offset, length); - return read; - } - -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java deleted file mode 100644 index d598cce8..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java +++ /dev/null @@ -1,564 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification; - -import java.io.BufferedInputStream; -import java.io.EOFException; -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Set; -import javax.annotation.Nonnull; - -import org.bouncycastle.bcpg.ArmoredInputStream; -import org.bouncycastle.openpgp.PGPCompressedData; -import org.bouncycastle.openpgp.PGPEncryptedData; -import org.bouncycastle.openpgp.PGPEncryptedDataList; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPLiteralData; -import org.bouncycastle.openpgp.PGPObjectFactory; -import org.bouncycastle.openpgp.PGPOnePassSignature; -import org.bouncycastle.openpgp.PGPOnePassSignatureList; -import org.bouncycastle.openpgp.PGPPBEEncryptedData; -import org.bouncycastle.openpgp.PGPPrivateKey; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.PGPSignatureList; -import org.bouncycastle.openpgp.PGPUtil; -import org.bouncycastle.openpgp.operator.KeyFingerPrintCalculator; -import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; -import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; -import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; -import org.pgpainless.PGPainless; -import org.pgpainless.algorithm.CompressionAlgorithm; -import org.pgpainless.algorithm.EncryptionPurpose; -import org.pgpainless.algorithm.StreamEncoding; -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; -import org.pgpainless.exception.MessageNotIntegrityProtectedException; -import org.pgpainless.exception.MissingDecryptionMethodException; -import org.pgpainless.exception.MissingLiteralDataException; -import org.pgpainless.exception.MissingPassphraseException; -import org.pgpainless.exception.SignatureValidationException; -import org.pgpainless.exception.UnacceptableAlgorithmException; -import org.pgpainless.exception.WrongConsumingMethodException; -import org.pgpainless.implementation.ImplementationFactory; -import org.pgpainless.key.SubkeyIdentifier; -import org.pgpainless.key.info.KeyRingInfo; -import org.pgpainless.key.protection.SecretKeyRingProtector; -import org.pgpainless.key.protection.UnlockSecretKey; -import org.pgpainless.signature.DetachedSignatureCheck; -import org.pgpainless.signature.OnePassSignatureCheck; -import org.pgpainless.signature.SignatureUtils; -import org.pgpainless.util.CRCingArmoredInputStreamWrapper; -import org.pgpainless.util.PGPUtilWrapper; -import org.pgpainless.util.Passphrase; -import org.pgpainless.util.Tuple; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public final class DecryptionStreamFactory { - - - private static final Logger LOGGER = LoggerFactory.getLogger(DecryptionStreamFactory.class); - private static final int MAX_RECURSION_DEPTH = 16; - - private final ConsumerOptions options; - private final OpenPgpMetadata.Builder resultBuilder = OpenPgpMetadata.getBuilder(); - private final List onePassSignatureChecks = new ArrayList<>(); - private final List detachedSignatureChecks = new ArrayList<>(); - private final Map onePassSignaturesWithMissingCert = new HashMap<>(); - - private static final PGPContentVerifierBuilderProvider verifierBuilderProvider = - ImplementationFactory.getInstance().getPGPContentVerifierBuilderProvider(); - private static final KeyFingerPrintCalculator keyFingerprintCalculator = - ImplementationFactory.getInstance().getKeyFingerprintCalculator(); - private IntegrityProtectedInputStream integrityProtectedEncryptedInputStream; - - - public static DecryptionStream create(@Nonnull InputStream inputStream, - @Nonnull ConsumerOptions options) - throws PGPException, IOException { - DecryptionStreamFactory factory = new DecryptionStreamFactory(options); - return factory.parseOpenPGPDataAndCreateDecryptionStream(inputStream); - } - - public DecryptionStreamFactory(ConsumerOptions options) { - this.options = options; - initializeDetachedSignatures(options.getDetachedSignatures()); - } - - private void initializeDetachedSignatures(Set signatures) { - for (PGPSignature signature : signatures) { - long issuerKeyId = SignatureUtils.determineIssuerKeyId(signature); - PGPPublicKeyRing signingKeyRing = findSignatureVerificationKeyRing(issuerKeyId); - if (signingKeyRing == null) { - SignatureValidationException ex = new SignatureValidationException("Missing verification certificate " + Long.toHexString(issuerKeyId)); - resultBuilder.addInvalidDetachedSignature(new SignatureVerification(signature, null), ex); - continue; - } - PGPPublicKey signingKey = signingKeyRing.getPublicKey(issuerKeyId); - SubkeyIdentifier signingKeyIdentifier = new SubkeyIdentifier(signingKeyRing, signingKey.getKeyID()); - try { - signature.init(verifierBuilderProvider, signingKey); - DetachedSignatureCheck detachedSignature = new DetachedSignatureCheck(signature, signingKeyRing, signingKeyIdentifier); - detachedSignatureChecks.add(detachedSignature); - } catch (PGPException e) { - SignatureValidationException ex = new SignatureValidationException("Cannot verify detached signature made by " + signingKeyIdentifier + ".", e); - resultBuilder.addInvalidDetachedSignature(new SignatureVerification(signature, signingKeyIdentifier), ex); - } - } - } - - private DecryptionStream parseOpenPGPDataAndCreateDecryptionStream(InputStream inputStream) throws IOException, PGPException { - // Make sure we handle armored and non-armored data properly - BufferedInputStream bufferedIn = new BufferedInputStream(inputStream); - InputStream decoderStream = PGPUtilWrapper.getDecoderStream(bufferedIn); - - decoderStream = CRCingArmoredInputStreamWrapper.possiblyWrap(decoderStream); - - if (decoderStream instanceof ArmoredInputStream) { - ArmoredInputStream armor = (ArmoredInputStream) decoderStream; - - if (armor.isClearText()) { - throw new WrongConsumingMethodException("Message appears to be using the Cleartext Signature Framework. " + - "Use PGPainless.verifyCleartextSignedMessage() to verify this message instead."); - } - } - - PGPObjectFactory objectFactory = new PGPObjectFactory( - decoderStream, keyFingerprintCalculator); - - try { - // Parse OpenPGP message - inputStream = processPGPPackets(objectFactory, 1); - } catch (EOFException e) { - throw e; - } catch (MissingLiteralDataException e) { - // Not an OpenPGP message. - // Reset the buffered stream to parse the message as arbitrary binary data - // to allow for detached signature verification. - LOGGER.debug("The message appears to not be an OpenPGP message. This is probably data signed with detached signatures?"); - bufferedIn.reset(); - inputStream = wrapInVerifySignatureStream(bufferedIn); - } catch (IOException e) { - if (e.getMessage().contains("invalid armor") || e.getMessage().contains("invalid header encountered")) { - // We falsely assumed the data to be armored. - LOGGER.debug("The message is apparently not armored."); - bufferedIn.reset(); - inputStream = wrapInVerifySignatureStream(bufferedIn); - } else { - throw e; - } - } - - return new DecryptionStream(inputStream, resultBuilder, integrityProtectedEncryptedInputStream, - (decoderStream instanceof ArmoredInputStream) ? decoderStream : null); - } - - private InputStream wrapInVerifySignatureStream(InputStream bufferedIn) { - return new SignatureInputStream.VerifySignatures( - bufferedIn, onePassSignatureChecks, - detachedSignatureChecks, options, - resultBuilder); - } - - private InputStream processPGPPackets(@Nonnull PGPObjectFactory objectFactory, int depth) throws IOException, PGPException { - if (depth >= MAX_RECURSION_DEPTH) { - throw new PGPException("Maximum recursion depth of packages exceeded."); - } - Object nextPgpObject; - while ((nextPgpObject = objectFactory.nextObject()) != null) { - if (nextPgpObject instanceof PGPEncryptedDataList) { - return processPGPEncryptedDataList((PGPEncryptedDataList) nextPgpObject, depth); - } - if (nextPgpObject instanceof PGPCompressedData) { - return processPGPCompressedData((PGPCompressedData) nextPgpObject, depth); - } - if (nextPgpObject instanceof PGPOnePassSignatureList) { - return processOnePassSignatureList(objectFactory, (PGPOnePassSignatureList) nextPgpObject, depth); - } - if (nextPgpObject instanceof PGPLiteralData) { - return processPGPLiteralData(objectFactory, (PGPLiteralData) nextPgpObject, depth); - } - } - - throw new MissingLiteralDataException("No Literal Data Packet found"); - } - - private InputStream processPGPEncryptedDataList(PGPEncryptedDataList pgpEncryptedDataList, int depth) - throws PGPException, IOException { - LOGGER.debug("Depth {}: Encountered PGPEncryptedDataList", depth); - InputStream decryptedDataStream = decryptSessionKey(pgpEncryptedDataList); - InputStream decodedDataStream = PGPUtil.getDecoderStream(decryptedDataStream); - PGPObjectFactory factory = new PGPObjectFactory(decodedDataStream, keyFingerprintCalculator); - return processPGPPackets(factory, ++depth); - } - - private InputStream processPGPCompressedData(PGPCompressedData pgpCompressedData, int depth) - throws PGPException, IOException { - CompressionAlgorithm compressionAlgorithm = CompressionAlgorithm.fromId(pgpCompressedData.getAlgorithm()); - LOGGER.debug("Depth {}: Encountered PGPCompressedData: {}", depth, compressionAlgorithm); - resultBuilder.setCompressionAlgorithm(compressionAlgorithm); - - InputStream inflatedDataStream = pgpCompressedData.getDataStream(); - InputStream decodedDataStream = PGPUtil.getDecoderStream(inflatedDataStream); - PGPObjectFactory objectFactory = new PGPObjectFactory(decodedDataStream, keyFingerprintCalculator); - - return processPGPPackets(objectFactory, ++depth); - } - - private InputStream processOnePassSignatureList(@Nonnull PGPObjectFactory objectFactory, PGPOnePassSignatureList onePassSignatures, int depth) - throws PGPException, IOException { - LOGGER.debug("Depth {}: Encountered PGPOnePassSignatureList of size {}", depth, onePassSignatures.size()); - initOnePassSignatures(onePassSignatures); - return processPGPPackets(objectFactory, ++depth); - } - - private InputStream processPGPLiteralData(@Nonnull PGPObjectFactory objectFactory, PGPLiteralData pgpLiteralData, int depth) throws IOException { - LOGGER.debug("Depth {}: Found PGPLiteralData", depth); - InputStream literalDataInputStream = pgpLiteralData.getInputStream(); - - resultBuilder.setFileName(pgpLiteralData.getFileName()) - .setModificationDate(pgpLiteralData.getModificationTime()) - .setFileEncoding(StreamEncoding.fromCode(pgpLiteralData.getFormat())); - - if (onePassSignatureChecks.isEmpty() && onePassSignaturesWithMissingCert.isEmpty()) { - LOGGER.debug("No OnePassSignatures found -> We are done"); - return literalDataInputStream; - } - - // Parse signatures from message - PGPSignatureList signatures = parseSignatures(objectFactory); - List signatureList = SignatureUtils.toList(signatures); - // Set signatures as comparison sigs in OPS checks - for (int i = 0; i < onePassSignatureChecks.size(); i++) { - int reversedIndex = onePassSignatureChecks.size() - i - 1; - onePassSignatureChecks.get(i).setSignature(signatureList.get(reversedIndex)); - } - - for (PGPSignature signature : signatureList) { - if (onePassSignaturesWithMissingCert.containsKey(signature.getKeyID())) { - OnePassSignatureCheck check = onePassSignaturesWithMissingCert.remove(signature.getKeyID()); - check.setSignature(signature); - - resultBuilder.addInvalidInbandSignature(new SignatureVerification(signature, null), - new SignatureValidationException("Missing verification certificate " + Long.toHexString(signature.getKeyID()))); - } - } - - return new SignatureInputStream.VerifySignatures(literalDataInputStream, - onePassSignatureChecks, detachedSignatureChecks, options, resultBuilder) { - }; - } - - private PGPSignatureList parseSignatures(PGPObjectFactory objectFactory) throws IOException { - PGPSignatureList signatureList = null; - Object pgpObject = objectFactory.nextObject(); - while (pgpObject != null && signatureList == null) { - if (pgpObject instanceof PGPSignatureList) { - signatureList = (PGPSignatureList) pgpObject; - } else { - pgpObject = objectFactory.nextObject(); - } - } - - if (signatureList == null || signatureList.isEmpty()) { - throw new IOException("Verification failed - No Signatures found"); - } - - return signatureList; - } - - private InputStream decryptSessionKey(@Nonnull PGPEncryptedDataList encryptedDataList) - throws PGPException { - Iterator encryptedDataIterator = encryptedDataList.getEncryptedDataObjects(); - if (!encryptedDataIterator.hasNext()) { - throw new PGPException("Decryption failed - EncryptedDataList has no items"); - } - - PGPPrivateKey decryptionKey = null; - PGPPublicKeyEncryptedData encryptedSessionKey = null; - - List passphraseProtected = new ArrayList<>(); - List publicKeyProtected = new ArrayList<>(); - List> postponedDueToMissingPassphrase = new ArrayList<>(); - - // Sort PKESK and SKESK packets - while (encryptedDataIterator.hasNext()) { - PGPEncryptedData encryptedData = encryptedDataIterator.next(); - - if (!encryptedData.isIntegrityProtected() && !options.isIgnoreMDCErrors()) { - throw new MessageNotIntegrityProtectedException(); - } - - // SKESK - if (encryptedData instanceof PGPPBEEncryptedData) { - passphraseProtected.add((PGPPBEEncryptedData) encryptedData); - } - // PKESK - else if (encryptedData instanceof PGPPublicKeyEncryptedData) { - publicKeyProtected.add((PGPPublicKeyEncryptedData) encryptedData); - } - } - - // Try decryption with passphrases first - for (PGPPBEEncryptedData pbeEncryptedData : passphraseProtected) { - for (Passphrase passphrase : options.getDecryptionPassphrases()) { - PBEDataDecryptorFactory passphraseDecryptor = ImplementationFactory.getInstance() - .getPBEDataDecryptorFactory(passphrase); - try { - InputStream decryptedDataStream = pbeEncryptedData.getDataStream(passphraseDecryptor); - - SymmetricKeyAlgorithm symmetricKeyAlgorithm = SymmetricKeyAlgorithm.fromId( - pbeEncryptedData.getSymmetricAlgorithm(passphraseDecryptor)); - throwIfAlgorithmIsRejected(symmetricKeyAlgorithm); - resultBuilder.setSymmetricKeyAlgorithm(symmetricKeyAlgorithm); - - integrityProtectedEncryptedInputStream = new IntegrityProtectedInputStream(decryptedDataStream, pbeEncryptedData, options); - - return integrityProtectedEncryptedInputStream; - } catch (PGPException e) { - LOGGER.debug("Probable passphrase mismatch, skip PBE encrypted data block", e); - } - } - } - - // Then try decryption with public key encryption - for (PGPPublicKeyEncryptedData publicKeyEncryptedData : publicKeyProtected) { - PGPPrivateKey privateKey = null; - if (options.getDecryptionKeys().isEmpty()) { - break; - } - - long keyId = publicKeyEncryptedData.getKeyID(); - // Wildcard KeyID - if (keyId == 0L) { - LOGGER.debug("Hidden recipient detected. Try to decrypt with all available secret keys."); - for (PGPSecretKeyRing secretKeys : options.getDecryptionKeys()) { - if (privateKey != null) { - break; - } - KeyRingInfo info = new KeyRingInfo(secretKeys); - List encryptionSubkeys = info.getEncryptionSubkeys(EncryptionPurpose.STORAGE_AND_COMMUNICATIONS); - for (PGPPublicKey pubkey : encryptionSubkeys) { - PGPSecretKey secretKey = secretKeys.getSecretKey(pubkey.getKeyID()); - // Skip missing secret key - if (secretKey == null) { - continue; - } - - privateKey = tryPublicKeyDecryption(secretKeys, secretKey, publicKeyEncryptedData, postponedDueToMissingPassphrase, true); - } - } - } - // Non-wildcard key-id - else { - LOGGER.debug("PGPEncryptedData is encrypted for key {}", Long.toHexString(keyId)); - resultBuilder.addRecipientKeyId(keyId); - - PGPSecretKeyRing secretKeys = findDecryptionKeyRing(keyId); - if (secretKeys == null) { - LOGGER.debug("Missing certificate of {}. Skip.", Long.toHexString(keyId)); - continue; - } - - PGPSecretKey secretKey = secretKeys.getSecretKey(keyId); - privateKey = tryPublicKeyDecryption(secretKeys, secretKey, publicKeyEncryptedData, postponedDueToMissingPassphrase, true); - } - if (privateKey == null) { - continue; - } - decryptionKey = privateKey; - encryptedSessionKey = publicKeyEncryptedData; - } - - // Try postponed keys with missing passphrases (will cause missing passphrase callbacks to fire) - if (encryptedSessionKey == null) { - - if (options.getMissingKeyPassphraseStrategy() == MissingKeyPassphraseStrategy.THROW_EXCEPTION) { - // Non-interactive mode: Throw an exception with all locked decryption keys - Set keyIds = new HashSet<>(); - for (Tuple k : postponedDueToMissingPassphrase) { - keyIds.add(k.getA()); - } - if (!keyIds.isEmpty()) { - throw new MissingPassphraseException(keyIds); - } - } - else if (options.getMissingKeyPassphraseStrategy() == MissingKeyPassphraseStrategy.INTERACTIVE) { - // Interactive mode: Fire protector callbacks to get passphrases interactively - for (Tuple missingPassphrases : postponedDueToMissingPassphrase) { - SubkeyIdentifier keyId = missingPassphrases.getA(); - PGPPublicKeyEncryptedData publicKeyEncryptedData = missingPassphrases.getB(); - PGPSecretKeyRing secretKeys = findDecryptionKeyRing(keyId.getKeyId()); - PGPSecretKey secretKey = secretKeys.getSecretKey(keyId.getSubkeyId()); - - PGPPrivateKey privateKey = tryPublicKeyDecryption(secretKeys, secretKey, publicKeyEncryptedData, postponedDueToMissingPassphrase, false); - if (privateKey == null) { - continue; - } - - decryptionKey = privateKey; - encryptedSessionKey = publicKeyEncryptedData; - break; - } - } else { - throw new IllegalStateException("Invalid PostponedKeysStrategy set in consumer options."); - } - - } - - return decryptWith(encryptedSessionKey, decryptionKey); - } - - /** - * Try decryption of the provided public-key-encrypted-data using the given secret key. - * If the secret key is encrypted and the secret key protector does not have a passphrase available and the boolean - * postponeIfMissingPassphrase is true, data decryption is postponed by pushing a tuple of the encrypted data decryption key - * identifier to the postponed list. - * - * This method only returns a non-null private key, if the private key is able to decrypt the message successfully. - * - * @param secretKeys secret key ring - * @param secretKey secret key - * @param publicKeyEncryptedData encrypted data which is tried to decrypt using the secret key - * @param postponed list of postponed decryptions due to missing secret key passphrases - * @param postponeIfMissingPassphrase flag to specify whether missing secret key passphrases should result in postponed decryption - * @return private key if decryption is successful, null if decryption is unsuccessful or postponed - * - * @throws PGPException in case of an OpenPGP error - */ - private PGPPrivateKey tryPublicKeyDecryption( - PGPSecretKeyRing secretKeys, - PGPSecretKey secretKey, - PGPPublicKeyEncryptedData publicKeyEncryptedData, - List> postponed, - boolean postponeIfMissingPassphrase) throws PGPException { - SecretKeyRingProtector protector = options.getSecretKeyProtector(secretKeys); - - if (postponeIfMissingPassphrase && !protector.hasPassphraseFor(secretKey.getKeyID())) { - // Postpone decryption with key with missing passphrase - SubkeyIdentifier identifier = new SubkeyIdentifier(secretKeys, secretKey.getKeyID()); - postponed.add(new Tuple<>(identifier, publicKeyEncryptedData)); - return null; - } - - PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey( - secretKey, protector.getDecryptor(secretKey.getKeyID())); - - // test if we have the right private key - PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() - .getPublicKeyDataDecryptorFactory(privateKey); - try { - publicKeyEncryptedData.getSymmetricAlgorithm(decryptorFactory); // will only succeed if we have the right secret key - LOGGER.debug("Found correct decryption key {}.", Long.toHexString(secretKey.getKeyID())); - resultBuilder.setDecryptionKey(new SubkeyIdentifier(secretKeys, privateKey.getKeyID())); - return privateKey; - } catch (PGPException | ClassCastException e) { - return null; - } - } - - private InputStream decryptWith(PGPPublicKeyEncryptedData encryptedSessionKey, PGPPrivateKey decryptionKey) - throws PGPException { - if (decryptionKey == null || encryptedSessionKey == null) { - throw new MissingDecryptionMethodException("Decryption failed - No suitable decryption key or passphrase found"); - } - - PublicKeyDataDecryptorFactory dataDecryptor = ImplementationFactory.getInstance() - .getPublicKeyDataDecryptorFactory(decryptionKey); - - SymmetricKeyAlgorithm symmetricKeyAlgorithm = SymmetricKeyAlgorithm - .fromId(encryptedSessionKey.getSymmetricAlgorithm(dataDecryptor)); - if (symmetricKeyAlgorithm == SymmetricKeyAlgorithm.NULL) { - LOGGER.debug("Message is unencrypted"); - } else { - LOGGER.debug("Message is encrypted using {}", symmetricKeyAlgorithm); - } - throwIfAlgorithmIsRejected(symmetricKeyAlgorithm); - resultBuilder.setSymmetricKeyAlgorithm(symmetricKeyAlgorithm); - - integrityProtectedEncryptedInputStream = new IntegrityProtectedInputStream(encryptedSessionKey.getDataStream(dataDecryptor), encryptedSessionKey, options); - return integrityProtectedEncryptedInputStream; - } - - private void throwIfAlgorithmIsRejected(SymmetricKeyAlgorithm algorithm) throws UnacceptableAlgorithmException { - if (!PGPainless.getPolicy().getSymmetricKeyDecryptionAlgorithmPolicy().isAcceptable(algorithm)) { - throw new UnacceptableAlgorithmException("Data is " - + (algorithm == SymmetricKeyAlgorithm.NULL ? "unencrypted" : "encrypted with symmetric algorithm " + algorithm) + " which is not acceptable as per PGPainless' policy.\n" + - "To mark this algorithm as acceptable, use PGPainless.getPolicy().setSymmetricKeyDecryptionAlgorithmPolicy()."); - } - } - - private void initOnePassSignatures(@Nonnull PGPOnePassSignatureList onePassSignatureList) throws PGPException { - Iterator iterator = onePassSignatureList.iterator(); - if (!iterator.hasNext()) { - throw new PGPException("Verification failed - No OnePassSignatures found"); - } - - processOnePassSignatures(iterator); - } - - private void processOnePassSignatures(Iterator signatures) throws PGPException { - while (signatures.hasNext()) { - PGPOnePassSignature signature = signatures.next(); - processOnePassSignature(signature); - } - } - - private void processOnePassSignature(PGPOnePassSignature signature) throws PGPException { - final long keyId = signature.getKeyID(); - - LOGGER.debug("Encountered OnePassSignature from {}", Long.toHexString(keyId)); - - // Find public key - PGPPublicKeyRing verificationKeyRing = findSignatureVerificationKeyRing(keyId); - if (verificationKeyRing == null) { - onePassSignaturesWithMissingCert.put(keyId, new OnePassSignatureCheck(signature, null)); - return; - } - PGPPublicKey verificationKey = verificationKeyRing.getPublicKey(keyId); - - signature.init(verifierBuilderProvider, verificationKey); - OnePassSignatureCheck onePassSignature = new OnePassSignatureCheck(signature, verificationKeyRing); - onePassSignatureChecks.add(onePassSignature); - } - - private PGPSecretKeyRing findDecryptionKeyRing(long keyId) { - for (PGPSecretKeyRing key : options.getDecryptionKeys()) { - if (key.getSecretKey(keyId) != null) { - return key; - } - } - return null; - } - - private PGPPublicKeyRing findSignatureVerificationKeyRing(long keyId) { - PGPPublicKeyRing verificationKeyRing = null; - for (PGPPublicKeyRing publicKeyRing : options.getCertificates()) { - PGPPublicKey verificationKey = publicKeyRing.getPublicKey(keyId); - if (verificationKey != null) { - LOGGER.debug("Found public key {} for signature verification", Long.toHexString(keyId)); - verificationKeyRing = publicKeyRing; - break; - } - } - - if (verificationKeyRing == null && options.getMissingCertificateCallback() != null) { - verificationKeyRing = options.getMissingCertificateCallback().onMissingPublicKeyEncountered(keyId); - } - - return verificationKeyRing; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/IntegrityProtectedInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/IntegrityProtectedInputStream.java deleted file mode 100644 index 4da52d0f..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/IntegrityProtectedInputStream.java +++ /dev/null @@ -1,49 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification; - -import java.io.IOException; -import java.io.InputStream; -import javax.annotation.Nonnull; - -import org.bouncycastle.openpgp.PGPEncryptedData; -import org.bouncycastle.openpgp.PGPException; -import org.pgpainless.exception.ModificationDetectionException; - -public class IntegrityProtectedInputStream extends InputStream { - - private final InputStream inputStream; - private final PGPEncryptedData encryptedData; - private final ConsumerOptions options; - - public IntegrityProtectedInputStream(InputStream inputStream, PGPEncryptedData encryptedData, ConsumerOptions options) { - this.inputStream = inputStream; - this.encryptedData = encryptedData; - this.options = options; - } - - @Override - public int read() throws IOException { - return inputStream.read(); - } - - @Override - public int read(@Nonnull byte[] b, int offset, int length) throws IOException { - return inputStream.read(b, offset, length); - } - - @Override - public void close() throws IOException { - if (encryptedData.isIntegrityProtected() && !options.isIgnoreMDCErrors()) { - try { - if (!encryptedData.verify()) { - throw new ModificationDetectionException(); - } - } catch (PGPException e) { - throw new IOException("Failed to verify integrity protection", e); - } - } - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageInspector.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageInspector.java deleted file mode 100644 index 1b94f877..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageInspector.java +++ /dev/null @@ -1,124 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification; - -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import org.bouncycastle.openpgp.PGPCompressedData; -import org.bouncycastle.openpgp.PGPEncryptedData; -import org.bouncycastle.openpgp.PGPEncryptedDataList; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPLiteralData; -import org.bouncycastle.openpgp.PGPObjectFactory; -import org.bouncycastle.openpgp.PGPOnePassSignatureList; -import org.bouncycastle.openpgp.PGPPBEEncryptedData; -import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData; -import org.pgpainless.implementation.ImplementationFactory; -import org.pgpainless.util.ArmorUtils; - -/** - * Inspect an OpenPGP message to determine IDs of its encryption keys or whether it is passphrase protected. - */ -public final class MessageInspector { - - public static class EncryptionInfo { - private final List keyIds = new ArrayList<>(); - private boolean isPassphraseEncrypted = false; - private boolean isSignedOnly = false; - - /** - * Return a list of recipient key ids for whom the message is encrypted. - * @return recipient key ids - */ - public List getKeyIds() { - return Collections.unmodifiableList(keyIds); - } - - public boolean isPassphraseEncrypted() { - return isPassphraseEncrypted; - } - - /** - * Return true, if the message is encrypted. - * - * @return true if encrypted - */ - public boolean isEncrypted() { - return isPassphraseEncrypted || !keyIds.isEmpty(); - } - - /** - * Return true, if the message is not encrypted, but signed using {@link org.bouncycastle.openpgp.PGPOnePassSignature OnePassSignatures}. - * - * @return true if message is signed only - */ - public boolean isSignedOnly() { - return isSignedOnly; - } - } - - private MessageInspector() { - - } - - /** - * Parses parts of the provided OpenPGP message in order to determine which keys were used to encrypt it. - * Note: This method does not rewind the passed in Stream, so you might need to take care of that yourselves. - * - * @param dataIn openpgp message - * @return encryption information - * @throws IOException - * @throws PGPException - */ - public static EncryptionInfo determineEncryptionInfoForMessage(InputStream dataIn) throws IOException, PGPException { - InputStream decoded = ArmorUtils.getDecoderStream(dataIn); - EncryptionInfo info = new EncryptionInfo(); - - processMessage(decoded, info); - - return info; - } - - private static void processMessage(InputStream dataIn, EncryptionInfo info) throws PGPException { - PGPObjectFactory objectFactory = new PGPObjectFactory(dataIn, - ImplementationFactory.getInstance().getKeyFingerprintCalculator()); - - for (Object next : objectFactory) { - if (next instanceof PGPOnePassSignatureList) { - PGPOnePassSignatureList signatures = (PGPOnePassSignatureList) next; - if (!signatures.isEmpty()) { - info.isSignedOnly = true; - return; - } - } - - if (next instanceof PGPEncryptedDataList) { - PGPEncryptedDataList encryptedDataList = (PGPEncryptedDataList) next; - for (PGPEncryptedData encryptedData : encryptedDataList) { - if (encryptedData instanceof PGPPublicKeyEncryptedData) { - PGPPublicKeyEncryptedData pubKeyEncryptedData = (PGPPublicKeyEncryptedData) encryptedData; - info.keyIds.add(pubKeyEncryptedData.getKeyID()); - } else if (encryptedData instanceof PGPPBEEncryptedData) { - info.isPassphraseEncrypted = true; - } - } - } - - if (next instanceof PGPCompressedData) { - PGPCompressedData compressed = (PGPCompressedData) next; - InputStream decompressed = compressed.getDataStream(); - processMessage(decompressed, info); - } - - if (next instanceof PGPLiteralData) { - return; - } - } - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MissingKeyPassphraseStrategy.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MissingKeyPassphraseStrategy.java deleted file mode 100644 index ed6a9c63..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MissingKeyPassphraseStrategy.java +++ /dev/null @@ -1,21 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification; - -/** - * Strategy defining how missing secret key passphrases are handled. - */ -public enum MissingKeyPassphraseStrategy { - /** - * Try to interactively obtain key passphrases one-by-one via callbacks, - * eg {@link org.pgpainless.key.protection.passphrase_provider.SecretKeyPassphraseProvider}. - */ - INTERACTIVE, - /** - * Do not try to obtain passphrases interactively and instead throw a - * {@link org.pgpainless.exception.MissingPassphraseException} listing all keys with missing passphrases. - */ - THROW_EXCEPTION -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MissingPublicKeyCallback.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MissingPublicKeyCallback.java deleted file mode 100644 index f3eb949b..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MissingPublicKeyCallback.java +++ /dev/null @@ -1,31 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import org.bouncycastle.openpgp.PGPPublicKeyRing; - -public interface MissingPublicKeyCallback { - - /** - * This method gets called if we encounter a signature made by a key which was not provided for signature verification. - * If you cannot provide the requested key, it is safe to return null here. - * PGPainless will then continue verification with the next signature. - * - * Note: The key-id might belong to a subkey, so be aware that when looking up the {@link PGPPublicKeyRing}, - * you may not only search for the key-id on the key rings primary key! - * - * It would be super cool to provide the OpenPgp fingerprint here, but unfortunately one-pass-signatures - * only contain the key id (see https://datatracker.ietf.org/doc/html/rfc4880#section-5.4) - * - * @param keyId ID of the missing signing (sub)key - * - * @return keyring containing the key or null - */ - @Nullable PGPPublicKeyRing onMissingPublicKeyEncountered(@Nonnull Long keyId); - -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpInputStream.java new file mode 100644 index 00000000..3522f509 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpInputStream.java @@ -0,0 +1,417 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification; + +import static org.bouncycastle.bcpg.PacketTags.COMPRESSED_DATA; +import static org.bouncycastle.bcpg.PacketTags.EXPERIMENTAL_1; +import static org.bouncycastle.bcpg.PacketTags.EXPERIMENTAL_2; +import static org.bouncycastle.bcpg.PacketTags.EXPERIMENTAL_3; +import static org.bouncycastle.bcpg.PacketTags.EXPERIMENTAL_4; +import static org.bouncycastle.bcpg.PacketTags.LITERAL_DATA; +import static org.bouncycastle.bcpg.PacketTags.MARKER; +import static org.bouncycastle.bcpg.PacketTags.MOD_DETECTION_CODE; +import static org.bouncycastle.bcpg.PacketTags.ONE_PASS_SIGNATURE; +import static org.bouncycastle.bcpg.PacketTags.PUBLIC_KEY; +import static org.bouncycastle.bcpg.PacketTags.PUBLIC_KEY_ENC_SESSION; +import static org.bouncycastle.bcpg.PacketTags.PUBLIC_SUBKEY; +import static org.bouncycastle.bcpg.PacketTags.RESERVED; +import static org.bouncycastle.bcpg.PacketTags.SECRET_KEY; +import static org.bouncycastle.bcpg.PacketTags.SECRET_SUBKEY; +import static org.bouncycastle.bcpg.PacketTags.SIGNATURE; +import static org.bouncycastle.bcpg.PacketTags.SYMMETRIC_KEY_ENC; +import static org.bouncycastle.bcpg.PacketTags.SYMMETRIC_KEY_ENC_SESSION; +import static org.bouncycastle.bcpg.PacketTags.SYM_ENC_INTEGRITY_PRO; +import static org.bouncycastle.bcpg.PacketTags.TRUST; +import static org.bouncycastle.bcpg.PacketTags.USER_ATTRIBUTE; +import static org.bouncycastle.bcpg.PacketTags.USER_ID; + +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.util.NoSuchElementException; + +import org.bouncycastle.bcpg.BCPGInputStream; +import org.bouncycastle.openpgp.PGPCompressedData; +import org.bouncycastle.openpgp.PGPEncryptedData; +import org.bouncycastle.openpgp.PGPLiteralData; +import org.bouncycastle.openpgp.PGPOnePassSignature; +import org.pgpainless.algorithm.CompressionAlgorithm; +import org.pgpainless.algorithm.HashAlgorithm; +import org.pgpainless.algorithm.PublicKeyAlgorithm; +import org.pgpainless.algorithm.SignatureType; +import org.pgpainless.algorithm.StreamEncoding; +import org.pgpainless.algorithm.SymmetricKeyAlgorithm; + +/** + * InputStream used to determine the nature of potential OpenPGP data. + */ +public class OpenPgpInputStream extends BufferedInputStream { + + @SuppressWarnings("CharsetObjectCanBeUsed") + private static final byte[] ARMOR_HEADER = "-----BEGIN PGP ".getBytes(Charset.forName("UTF8")); + + // Buffer beginning bytes of the data + public static final int MAX_BUFFER_SIZE = 8192 * 2; + + private final byte[] buffer; + private final int bufferLen; + + private boolean containsArmorHeader; + private boolean containsOpenPgpPackets; + private boolean isLikelyOpenPgpMessage; + + public OpenPgpInputStream(InputStream in, boolean check) throws IOException { + super(in, MAX_BUFFER_SIZE); + + mark(MAX_BUFFER_SIZE); + buffer = new byte[MAX_BUFFER_SIZE]; + bufferLen = read(buffer); + reset(); + + if (check) { + inspectBuffer(); + } + } + + public OpenPgpInputStream(InputStream in) throws IOException { + this(in, true); + } + + private void inspectBuffer() throws IOException { + if (checkForAsciiArmor()) { + return; + } + + checkForBinaryOpenPgp(); + } + + private boolean checkForAsciiArmor() { + if (startsWithIgnoringWhitespace(buffer, ARMOR_HEADER, bufferLen)) { + containsArmorHeader = true; + return true; + } + return false; + } + + /** + * This method is still brittle. + * Basically we try to parse OpenPGP packets from the buffer. + * If we run into exceptions, then we know that the data is non-OpenPGP'ish. + * + * This breaks down though if we read plausible garbage where the data accidentally makes sense, + * or valid, yet incomplete packets (remember, we are still only working on a portion of the data). + */ + private void checkForBinaryOpenPgp() throws IOException { + if (bufferLen == -1) { + // Empty data + return; + } + + ByteArrayInputStream bufferIn = new ByteArrayInputStream(buffer, 0, bufferLen); + nonExhaustiveParseAndCheckPlausibility(bufferIn); + } + + private void nonExhaustiveParseAndCheckPlausibility(ByteArrayInputStream bufferIn) throws IOException { + // Read the packet header + int hdr = bufferIn.read(); + if (hdr < 0 || (hdr & 0x80) == 0) { + return; + } + + boolean newPacket = (hdr & 0x40) != 0; + int tag = 0; + int bodyLen = 0; + boolean partial = false; + + // Determine the packet length + if (newPacket) { + tag = hdr & 0x3f; + + int l = bufferIn.read(); + if (l < 192) { + bodyLen = l; + } else if (l <= 223) { + int b = bufferIn.read(); + bodyLen = ((l - 192) << 8) + (b) + 192; + } else if (l == 255) { + bodyLen = (bufferIn.read() << 24) | (bufferIn.read() << 16) | (bufferIn.read() << 8) | bufferIn.read(); + } else { + partial = true; + bodyLen = 1 << (l & 0x1f); + } + } else { + int lengthType = hdr & 0x3; + tag = (hdr & 0x3f) >> 2; + switch (lengthType) { + case 0: + bodyLen = bufferIn.read(); + break; + case 1: + bodyLen = (bufferIn.read() << 8) | bufferIn.read(); + break; + case 2: + bodyLen = (bufferIn.read() << 24) | (bufferIn.read() << 16) | (bufferIn.read() << 8) | bufferIn.read(); + break; + case 3: + partial = true; + break; + default: + return; + } + } + + // Negative body length -> garbage + if (bodyLen < 0) { + return; + } + + // Try to unexhaustively parse the first packet bit by bit and check for plausibility + BCPGInputStream bcpgIn = new BCPGInputStream(bufferIn); + switch (tag) { + case RESERVED: + // How to handle this? Probably discard as garbage... + return; + + case PUBLIC_KEY_ENC_SESSION: + int pkeskVersion = bcpgIn.read(); + if (pkeskVersion <= 0 || pkeskVersion > 5) { + return; + } + + // Skip Key-ID + for (int i = 0; i < 8; i++) { + bcpgIn.read(); + } + + int pkeskAlg = bcpgIn.read(); + if (PublicKeyAlgorithm.fromId(pkeskAlg) == null) { + return; + } + + containsOpenPgpPackets = true; + isLikelyOpenPgpMessage = true; + break; + + case SIGNATURE: + int sigVersion = bcpgIn.read(); + int sigType; + if (sigVersion == 2 || sigVersion == 3) { + int l = bcpgIn.read(); + sigType = bcpgIn.read(); + } else if (sigVersion == 4 || sigVersion == 5) { + sigType = bcpgIn.read(); + } else { + return; + } + + try { + SignatureType.requireFromCode(sigType); + } catch (NoSuchElementException e) { + return; + } + + containsOpenPgpPackets = true; + break; + + case SYMMETRIC_KEY_ENC_SESSION: + int skeskVersion = bcpgIn.read(); + if (skeskVersion == 4) { + int skeskAlg = bcpgIn.read(); + if (SymmetricKeyAlgorithm.fromId(skeskAlg) == null) { + return; + } + // TODO: Parse S2K? + } else { + return; + } + containsOpenPgpPackets = true; + isLikelyOpenPgpMessage = true; + break; + + case ONE_PASS_SIGNATURE: + int opsVersion = bcpgIn.read(); + if (opsVersion == 3) { + int opsSigType = bcpgIn.read(); + try { + SignatureType.requireFromCode(opsSigType); + } catch (NoSuchElementException e) { + return; + } + int opsHashAlg = bcpgIn.read(); + if (HashAlgorithm.fromId(opsHashAlg) == null) { + return; + } + int opsKeyAlg = bcpgIn.read(); + if (PublicKeyAlgorithm.fromId(opsKeyAlg) == null) { + return; + } + } else { + return; + } + + containsOpenPgpPackets = true; + isLikelyOpenPgpMessage = true; + break; + + case SECRET_KEY: + case PUBLIC_KEY: + case SECRET_SUBKEY: + case PUBLIC_SUBKEY: + int keyVersion = bcpgIn.read(); + for (int i = 0; i < 4; i++) { + // Creation time + bcpgIn.read(); + } + if (keyVersion == 3) { + long validDays = (in.read() << 8) | in.read(); + if (validDays < 0) { + return; + } + } else if (keyVersion == 4) { + + } else if (keyVersion == 5) { + + } else { + return; + } + int keyAlg = bcpgIn.read(); + if (PublicKeyAlgorithm.fromId(keyAlg) == null) { + return; + } + + containsOpenPgpPackets = true; + break; + + case COMPRESSED_DATA: + int compAlg = bcpgIn.read(); + if (CompressionAlgorithm.fromId(compAlg) == null) { + return; + } + + containsOpenPgpPackets = true; + isLikelyOpenPgpMessage = true; + break; + + case SYMMETRIC_KEY_ENC: + // No data to compare :( + containsOpenPgpPackets = true; + // While this is a valid OpenPGP message, enabling the line below would lead to too many false positives + // isLikelyOpenPgpMessage = true; + break; + + case MARKER: + byte[] marker = new byte[3]; + bcpgIn.readFully(marker); + if (marker[0] != 0x50 || marker[1] != 0x47 || marker[2] != 0x50) { + return; + } + + containsOpenPgpPackets = true; + break; + + case LITERAL_DATA: + int format = bcpgIn.read(); + if (StreamEncoding.fromCode(format) == null) { + return; + } + + containsOpenPgpPackets = true; + isLikelyOpenPgpMessage = true; + break; + + case TRUST: + case USER_ID: + case USER_ATTRIBUTE: + // Not much to compare + containsOpenPgpPackets = true; + break; + + case SYM_ENC_INTEGRITY_PRO: + int seipVersion = bcpgIn.read(); + if (seipVersion != 1) { + return; + } + isLikelyOpenPgpMessage = true; + containsOpenPgpPackets = true; + break; + + case MOD_DETECTION_CODE: + byte[] digest = new byte[20]; + bcpgIn.readFully(digest); + + containsOpenPgpPackets = true; + break; + + case EXPERIMENTAL_1: + case EXPERIMENTAL_2: + case EXPERIMENTAL_3: + case EXPERIMENTAL_4: + return; + default: + containsOpenPgpPackets = false; + break; + } + } + + private boolean startsWithIgnoringWhitespace(byte[] bytes, byte[] subsequence, int bufferLen) { + if (bufferLen == -1) { + return false; + } + + for (int i = 0; i < bufferLen; i++) { + // Working on bytes is not trivial with unicode data, but its good enough here + if (Character.isWhitespace(bytes[i])) { + continue; + } + + if ((i + subsequence.length) > bytes.length) { + return false; + } + + for (int j = 0; j < subsequence.length; j++) { + if (bytes[i + j] != subsequence[j]) { + return false; + } + } + return true; + } + return false; + } + + public boolean isAsciiArmored() { + return containsArmorHeader; + } + + /** + * Return true, if the data is possibly binary OpenPGP. + * The criterion for this are less strict than for {@link #isLikelyOpenPgpMessage()}, + * as it also accepts other OpenPGP packets at the beginning of the data stream. + * + * Use with caution. + * + * @return true if data appears to be binary OpenPGP data + */ + public boolean isBinaryOpenPgp() { + return containsOpenPgpPackets; + } + + /** + * Returns true, if the underlying data is very likely (more than 99,9%) an OpenPGP message. + * OpenPGP Message means here that it starts with either an {@link PGPEncryptedData}, + * {@link PGPCompressedData}, {@link PGPOnePassSignature} or {@link PGPLiteralData} packet. + * The plausability of these data packets is checked as far as possible. + * + * @return true if likely OpenPGP message + */ + public boolean isLikelyOpenPgpMessage() { + return isLikelyOpenPgpMessage; + } + + public boolean isNonOpenPgp() { + return !isAsciiArmored() && !isBinaryOpenPgp(); + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMetadata.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMetadata.java deleted file mode 100644 index 48afd5af..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMetadata.java +++ /dev/null @@ -1,347 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Date; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import org.bouncycastle.openpgp.PGPLiteralData; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSignature; -import org.pgpainless.algorithm.CompressionAlgorithm; -import org.pgpainless.algorithm.StreamEncoding; -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; -import org.pgpainless.exception.SignatureValidationException; -import org.pgpainless.key.OpenPgpV4Fingerprint; -import org.pgpainless.key.SubkeyIdentifier; - -public class OpenPgpMetadata { - - private final Set recipientKeyIds; - private final SubkeyIdentifier decryptionKey; - private final List verifiedInbandSignatures; - private final List invalidInbandSignatures; - private final List verifiedDetachedSignatures; - private final List invalidDetachedSignatures; - private final SymmetricKeyAlgorithm symmetricKeyAlgorithm; - private final CompressionAlgorithm compressionAlgorithm; - private final String fileName; - private final Date modificationDate; - private final StreamEncoding fileEncoding; - - public OpenPgpMetadata(Set recipientKeyIds, - SubkeyIdentifier decryptionKey, - SymmetricKeyAlgorithm symmetricKeyAlgorithm, - CompressionAlgorithm algorithm, - List verifiedInbandSignatures, - List invalidInbandSignatures, - List verifiedDetachedSignatures, - List invalidDetachedSignatures, - String fileName, - Date modificationDate, - StreamEncoding fileEncoding) { - - this.recipientKeyIds = Collections.unmodifiableSet(recipientKeyIds); - this.decryptionKey = decryptionKey; - this.symmetricKeyAlgorithm = symmetricKeyAlgorithm; - this.compressionAlgorithm = algorithm; - this.verifiedInbandSignatures = Collections.unmodifiableList(verifiedInbandSignatures); - this.invalidInbandSignatures = Collections.unmodifiableList(invalidInbandSignatures); - this.verifiedDetachedSignatures = Collections.unmodifiableList(verifiedDetachedSignatures); - this.invalidDetachedSignatures = Collections.unmodifiableList(invalidDetachedSignatures); - this.fileName = fileName; - this.modificationDate = modificationDate; - this.fileEncoding = fileEncoding; - } - - /** - * Return a set of key-ids the messages was encrypted for. - * - * @return recipient ids - */ - public @Nonnull Set getRecipientKeyIds() { - return recipientKeyIds; - } - - /** - * Return true, if the message was encrypted. - * - * @return true if encrypted, false otherwise - */ - public boolean isEncrypted() { - return symmetricKeyAlgorithm != SymmetricKeyAlgorithm.NULL && !getRecipientKeyIds().isEmpty(); - } - - /** - * Return the {@link SubkeyIdentifier} of the key that was used to decrypt the message. - * This can be null if the message was decrypted using a {@link org.pgpainless.util.Passphrase}, or if it was not - * encrypted at all (eg. signed only). - * - * @return subkey identifier of decryption key - */ - public @Nullable SubkeyIdentifier getDecryptionKey() { - return decryptionKey; - } - - /** - * Return the algorithm that was used to symmetrically encrypt the message. - * - * @return encryption algorithm - */ - public @Nullable SymmetricKeyAlgorithm getSymmetricKeyAlgorithm() { - return symmetricKeyAlgorithm; - } - - /** - * Return the {@link CompressionAlgorithm} that was used to compress the message. - * - * @return compression algorithm - */ - public @Nullable CompressionAlgorithm getCompressionAlgorithm() { - return compressionAlgorithm; - } - - /** - * Return a set of all signatures on the message. - * Note: This method returns just the signatures. There is no guarantee that the signatures are verified or even correct. - * - * Use {@link #getVerifiedSignatures()} instead to get all verified signatures. - * @return unverified and verified signatures - */ - public @Nonnull Set getSignatures() { - Set signatures = new HashSet<>(); - for (SignatureVerification v : getVerifiedDetachedSignatures()) { - signatures.add(v.getSignature()); - } - for (SignatureVerification v : getVerifiedInbandSignatures()) { - signatures.add(v.getSignature()); - } - for (SignatureVerification.Failure f : getInvalidDetachedSignatures()) { - signatures.add(f.getSignatureVerification().getSignature()); - } - for (SignatureVerification.Failure f : getInvalidInbandSignatures()) { - signatures.add(f.getSignatureVerification().getSignature()); - } - return signatures; - } - - /** - * Return true if the message contained at least one signature. - * - * Note: This method does not reflect, whether the signature on the message is correct. - * Use {@link #isVerified()} instead to determine, if the message carries a verifiable signature. - * - * @return true if message contains at least one unverified or verified signature, false otherwise. - */ - public boolean isSigned() { - return !getSignatures().isEmpty(); - } - - /** - * Return a map of all verified signatures on the message. - * The map contains verified signatures as value, with the {@link SubkeyIdentifier} of the key that was used to verify - * the signature as the maps keys. - * - * @return verified detached and one-pass signatures - */ - public Map getVerifiedSignatures() { - Map verifiedSignatures = new ConcurrentHashMap<>(); - for (SignatureVerification detachedSignature : getVerifiedDetachedSignatures()) { - verifiedSignatures.put(detachedSignature.getSigningKey(), detachedSignature.getSignature()); - } - for (SignatureVerification inbandSignatures : verifiedInbandSignatures) { - verifiedSignatures.put(inbandSignatures.getSigningKey(), inbandSignatures.getSignature()); - } - - return verifiedSignatures; - } - - public List getVerifiedInbandSignatures() { - return verifiedInbandSignatures; - } - - public List getVerifiedDetachedSignatures() { - return verifiedDetachedSignatures; - } - - public List getInvalidInbandSignatures() { - return invalidInbandSignatures; - } - - public List getInvalidDetachedSignatures() { - return invalidDetachedSignatures; - } - - /** - * Return true, if the message is signed and at least one signature on the message was verified successfully. - * - * @return true if message is verified, false otherwise - */ - public boolean isVerified() { - return !getVerifiedSignatures().isEmpty(); - } - - /** - * Return true, if the message contains at least one verified signature made by a key in the - * given certificate. - * - * @param certificate certificate - * @return true if message was signed by the certificate (and the signature is valid), false otherwise - */ - public boolean containsVerifiedSignatureFrom(PGPPublicKeyRing certificate) { - for (PGPPublicKey key : certificate) { - OpenPgpV4Fingerprint fingerprint = new OpenPgpV4Fingerprint(key); - if (containsVerifiedSignatureFrom(fingerprint)) { - return true; - } - } - return false; - } - - /** - * Return true, if the message contains at least one valid signature made by the key with the given - * fingerprint, false otherwise. - * - * The fingerprint might be of the signing subkey, or the primary key of the signing certificate. - * - * @param fingerprint fingerprint of primary key or signing subkey - * @return true if validly signed, false otherwise - */ - public boolean containsVerifiedSignatureFrom(OpenPgpV4Fingerprint fingerprint) { - for (SubkeyIdentifier verifiedSigningKey : getVerifiedSignatures().keySet()) { - if (verifiedSigningKey.getPrimaryKeyFingerprint().equals(fingerprint) || - verifiedSigningKey.getSubkeyFingerprint().equals(fingerprint)) { - return true; - } - } - return false; - } - - /** - * Return the name of the encrypted / signed file. - * - * @return file name - */ - public String getFileName() { - return fileName; - } - - /** - * Return true, if the encrypted data is intended for your eyes only. - * - * @return true if for-your-eyes-only - */ - public boolean isForYourEyesOnly() { - return PGPLiteralData.CONSOLE.equals(getFileName()); - } - - /** - * Return the modification date of the encrypted / signed file. - * - * @return modification date - */ - public Date getModificationDate() { - return modificationDate; - } - - /** - * Return the encoding format of the encrypted / signed file. - * - * @return encoding - */ - public StreamEncoding getFileEncoding() { - return fileEncoding; - } - - public static Builder getBuilder() { - return new Builder(); - } - - public static class Builder { - - private final Set recipientFingerprints = new HashSet<>(); - private SubkeyIdentifier decryptionKey; - private SymmetricKeyAlgorithm symmetricKeyAlgorithm = SymmetricKeyAlgorithm.NULL; - private CompressionAlgorithm compressionAlgorithm = CompressionAlgorithm.UNCOMPRESSED; - private String fileName; - private StreamEncoding fileEncoding; - private Date modificationDate; - - private final List verifiedInbandSignatures = new ArrayList<>(); - private final List verifiedDetachedSignatures = new ArrayList<>(); - private final List invalidInbandSignatures = new ArrayList<>(); - private final List invalidDetachedSignatures = new ArrayList<>(); - - - public Builder addRecipientKeyId(Long keyId) { - this.recipientFingerprints.add(keyId); - return this; - } - - public Builder setDecryptionKey(SubkeyIdentifier decryptionKey) { - this.decryptionKey = decryptionKey; - return this; - } - - public Builder setCompressionAlgorithm(CompressionAlgorithm algorithm) { - this.compressionAlgorithm = algorithm; - return this; - } - - public Builder setSymmetricKeyAlgorithm(SymmetricKeyAlgorithm symmetricKeyAlgorithm) { - this.symmetricKeyAlgorithm = symmetricKeyAlgorithm; - return this; - } - - public Builder setFileName(@Nonnull String fileName) { - this.fileName = fileName; - return this; - } - - public Builder setModificationDate(Date modificationDate) { - this.modificationDate = modificationDate; - return this; - } - - public Builder setFileEncoding(StreamEncoding encoding) { - this.fileEncoding = encoding; - return this; - } - - public OpenPgpMetadata build() { - return new OpenPgpMetadata( - recipientFingerprints, decryptionKey, - symmetricKeyAlgorithm, compressionAlgorithm, - verifiedInbandSignatures, invalidInbandSignatures, - verifiedDetachedSignatures, invalidDetachedSignatures, - fileName, modificationDate, fileEncoding); - } - - public void addVerifiedInbandSignature(SignatureVerification signatureVerification) { - this.verifiedInbandSignatures.add(signatureVerification); - } - - public void addVerifiedDetachedSignature(SignatureVerification signatureVerification) { - this.verifiedDetachedSignatures.add(signatureVerification); - } - - public void addInvalidInbandSignature(SignatureVerification signatureVerification, SignatureValidationException e) { - this.invalidInbandSignatures.add(new SignatureVerification.Failure(signatureVerification, e)); - } - - public void addInvalidDetachedSignature(SignatureVerification signatureVerification, SignatureValidationException e) { - this.invalidDetachedSignatures.add(new SignatureVerification.Failure(signatureVerification, e)); - } - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/SignatureInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/SignatureInputStream.java deleted file mode 100644 index 6166d6f6..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/SignatureInputStream.java +++ /dev/null @@ -1,143 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification; - -import static org.pgpainless.signature.SignatureValidator.signatureWasCreatedInBounds; - -import java.io.FilterInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.List; -import javax.annotation.Nonnull; - -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.pgpainless.PGPainless; -import org.pgpainless.exception.SignatureValidationException; -import org.pgpainless.policy.Policy; -import org.pgpainless.signature.CertificateValidator; -import org.pgpainless.signature.DetachedSignatureCheck; -import org.pgpainless.signature.OnePassSignatureCheck; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public abstract class SignatureInputStream extends FilterInputStream { - - protected SignatureInputStream(InputStream inputStream) { - super(inputStream); - } - - public static class VerifySignatures extends SignatureInputStream { - - private static final Logger LOGGER = LoggerFactory.getLogger(VerifySignatures.class); - - private final List opSignatures; - private final List detachedSignatures; - private final ConsumerOptions options; - private final OpenPgpMetadata.Builder resultBuilder; - - public VerifySignatures( - InputStream literalDataStream, - List opSignatures, - List detachedSignatures, - ConsumerOptions options, - OpenPgpMetadata.Builder resultBuilder) { - super(literalDataStream); - this.opSignatures = opSignatures; - this.detachedSignatures = detachedSignatures; - this.options = options; - this.resultBuilder = resultBuilder; - } - - @Override - public int read() throws IOException { - final int data = super.read(); - final boolean endOfStream = data == -1; - if (endOfStream) { - verifyOnePassSignatures(); - verifyDetachedSignatures(); - } else { - byte b = (byte) data; - updateOnePassSignatures(b); - updateDetachedSignatures(b); - } - return data; - } - - @Override - public int read(@Nonnull byte[] b, int off, int len) throws IOException { - int read = super.read(b, off, len); - - final boolean endOfStream = read == -1; - if (endOfStream) { - verifyOnePassSignatures(); - verifyDetachedSignatures(); - } else { - updateOnePassSignatures(b, off, read); - updateDetachedSignatures(b, off, read); - } - return read; - } - - private synchronized void verifyOnePassSignatures() { - Policy policy = PGPainless.getPolicy(); - for (OnePassSignatureCheck opSignature : opSignatures) { - if (opSignature.getSignature() == null) { - LOGGER.warn("Found OnePassSignature without respective signature packet -> skip"); - continue; - } - - try { - signatureWasCreatedInBounds(options.getVerifyNotBefore(), options.getVerifyNotAfter()).verify(opSignature.getSignature()); - CertificateValidator.validateCertificateAndVerifyOnePassSignature(opSignature, policy); - resultBuilder.addVerifiedInbandSignature(new SignatureVerification(opSignature.getSignature(), opSignature.getSigningKey())); - } catch (SignatureValidationException e) { - LOGGER.warn("One-pass-signature verification failed for signature made by key {}: {}", - opSignature.getSigningKey(), e.getMessage(), e); - resultBuilder.addInvalidInbandSignature(new SignatureVerification(opSignature.getSignature(), opSignature.getSigningKey()), e); - } - } - } - - private void verifyDetachedSignatures() { - Policy policy = PGPainless.getPolicy(); - for (DetachedSignatureCheck s : detachedSignatures) { - try { - signatureWasCreatedInBounds(options.getVerifyNotBefore(), options.getVerifyNotAfter()).verify(s.getSignature()); - CertificateValidator.validateCertificateAndVerifyInitializedSignature(s.getSignature(), (PGPPublicKeyRing) s.getSigningKeyRing(), policy); - resultBuilder.addVerifiedDetachedSignature(new SignatureVerification(s.getSignature(), s.getSigningKeyIdentifier())); - } catch (SignatureValidationException e) { - LOGGER.warn("One-pass-signature verification failed for signature made by key {}: {}", - s.getSigningKeyIdentifier(), e.getMessage(), e); - resultBuilder.addInvalidDetachedSignature(new SignatureVerification(s.getSignature(), s.getSigningKeyIdentifier()), e); - } - } - } - - private void updateOnePassSignatures(byte data) { - for (OnePassSignatureCheck opSignature : opSignatures) { - opSignature.getOnePassSignature().update(data); - } - } - - private void updateOnePassSignatures(byte[] bytes, int offset, int length) { - for (OnePassSignatureCheck opSignature : opSignatures) { - opSignature.getOnePassSignature().update(bytes, offset, length); - } - } - - private void updateDetachedSignatures(byte b) { - for (DetachedSignatureCheck detachedSignature : detachedSignatures) { - detachedSignature.getSignature().update(b); - } - } - - private void updateDetachedSignatures(byte[] b, int off, int read) { - for (DetachedSignatureCheck detachedSignature : detachedSignatures) { - detachedSignature.getSignature().update(b, off, read); - } - } - - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/SignatureVerification.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/SignatureVerification.java deleted file mode 100644 index 6063d72a..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/SignatureVerification.java +++ /dev/null @@ -1,94 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification; - -import javax.annotation.Nullable; - -import org.bouncycastle.openpgp.PGPSignature; -import org.pgpainless.exception.SignatureValidationException; -import org.pgpainless.key.SubkeyIdentifier; - -/** - * Tuple of a signature and an identifier of its corresponding verification key. - * Semantic meaning of the signature verification (success, failure) is merely given by context. - * E.g. {@link OpenPgpMetadata#getVerifiedInbandSignatures()} contains verified verifications, - * while the class {@link Failure} contains failed verifications. - */ -public class SignatureVerification { - - private final PGPSignature signature; - private final SubkeyIdentifier signingKey; - - /** - * Construct a verification tuple. - * - * @param signature PGPSignature object - * @param signingKey identifier of the signing key - */ - public SignatureVerification(PGPSignature signature, @Nullable SubkeyIdentifier signingKey) { - this.signature = signature; - this.signingKey = signingKey; - } - - /** - * Return the {@link PGPSignature}. - * - * @return signature - */ - public PGPSignature getSignature() { - return signature; - } - - /** - * Return a {@link SubkeyIdentifier} of the (sub-) key that is used for signature verification. - * Note, that this method might return null, e.g. in case of a {@link Failure} due to missing verification key. - * - * @return verification key identifier - */ - @Nullable - public SubkeyIdentifier getSigningKey() { - return signingKey; - } - - /** - * Tuple object of a {@link SignatureVerification} and the corresponding {@link SignatureValidationException} - * that caused the verification to fail. - */ - public static class Failure { - - private final SignatureVerification signatureVerification; - private final SignatureValidationException validationException; - - /** - * Construct a signature verification failure object. - * - * @param verification verification - * @param validationException exception that caused the verification to fail - */ - public Failure(SignatureVerification verification, SignatureValidationException validationException) { - this.signatureVerification = verification; - this.validationException = validationException; - } - - /** - * Return the verification (tuple of {@link PGPSignature} and corresponding {@link SubkeyIdentifier} - * of the signing/verification key. - * - * @return verification - */ - public SignatureVerification getSignatureVerification() { - return signatureVerification; - } - - /** - * Return the {@link SignatureValidationException} that caused the verification to fail. - * - * @return exception - */ - public SignatureValidationException getValidationException() { - return validationException; - } - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/ClearsignedMessageUtil.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/ClearsignedMessageUtil.java deleted file mode 100644 index feea89eb..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/ClearsignedMessageUtil.java +++ /dev/null @@ -1,161 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification.cleartext_signatures; - -import java.io.BufferedOutputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -import org.bouncycastle.bcpg.ArmoredInputStream; -import org.bouncycastle.openpgp.PGPObjectFactory; -import org.bouncycastle.openpgp.PGPSignatureList; -import org.bouncycastle.util.Strings; -import org.pgpainless.exception.WrongConsumingMethodException; -import org.pgpainless.implementation.ImplementationFactory; -import org.pgpainless.util.ArmoredInputStreamFactory; - -/** - * Utility class to deal with cleartext-signed messages. - * Based on Bouncycastle's {@link org.bouncycastle.openpgp.examples.ClearSignedFileProcessor}. - */ -public final class ClearsignedMessageUtil { - - private ClearsignedMessageUtil() { - - } - - /** - * Dearmor a clearsigned message, detach the inband signatures and write the plaintext message to the provided - * messageOutputStream. - * - * @param clearsignedInputStream input stream containing a clearsigned message - * @param messageOutputStream output stream to which the dearmored message shall be written - * @return signatures - * @throws IOException if the message is not clearsigned or some other IO error happens - */ - public static PGPSignatureList detachSignaturesFromInbandClearsignedMessage(InputStream clearsignedInputStream, - OutputStream messageOutputStream) - throws IOException, WrongConsumingMethodException { - ArmoredInputStream in = ArmoredInputStreamFactory.get(clearsignedInputStream); - if (!in.isClearText()) { - throw new WrongConsumingMethodException("Message is not using the Cleartext Signature Framework."); - } - - OutputStream out = new BufferedOutputStream(messageOutputStream); - try { - ByteArrayOutputStream lineOut = new ByteArrayOutputStream(); - int lookAhead = readInputLine(lineOut, in); - byte[] lineSep = getLineSeparator(); - - if (lookAhead != -1 && in.isClearText()) { - byte[] line = lineOut.toByteArray(); - out.write(line, 0, getLengthWithoutSeparatorOrTrailingWhitespace(line)); - - while (lookAhead != -1 && in.isClearText()) { - lookAhead = readInputLine(lineOut, lookAhead, in); - line = lineOut.toByteArray(); - out.write(lineSep); - out.write(line, 0, getLengthWithoutSeparatorOrTrailingWhitespace(line)); - } - } else { - if (lookAhead != -1) { - byte[] line = lineOut.toByteArray(); - out.write(line, 0, getLengthWithoutSeparatorOrTrailingWhitespace(line)); - } - } - } finally { - out.close(); - } - - PGPObjectFactory objectFactory = new PGPObjectFactory(in, ImplementationFactory.getInstance().getKeyFingerprintCalculator()); - PGPSignatureList signatures = (PGPSignatureList) objectFactory.nextObject(); - - return signatures; - } - - public static int readInputLine(ByteArrayOutputStream bOut, InputStream fIn) - throws IOException { - bOut.reset(); - - int lookAhead = -1; - int ch; - - while ((ch = fIn.read()) >= 0) { - bOut.write(ch); - if (ch == '\r' || ch == '\n') { - lookAhead = readPassedEOL(bOut, ch, fIn); - break; - } - } - - return lookAhead; - } - - public static int readInputLine(ByteArrayOutputStream bOut, int lookAhead, InputStream fIn) - throws IOException { - bOut.reset(); - - int ch = lookAhead; - - do { - bOut.write(ch); - if (ch == '\r' || ch == '\n') { - lookAhead = readPassedEOL(bOut, ch, fIn); - break; - } - } - while ((ch = fIn.read()) >= 0); - - if (ch < 0) { - lookAhead = -1; - } - - return lookAhead; - } - - private static int readPassedEOL(ByteArrayOutputStream bOut, int lastCh, InputStream fIn) - throws IOException { - int lookAhead = fIn.read(); - - if (lastCh == '\r' && lookAhead == '\n') { - bOut.write(lookAhead); - lookAhead = fIn.read(); - } - - return lookAhead; - } - - - private static byte[] getLineSeparator() { - String nl = Strings.lineSeparator(); - byte[] nlBytes = new byte[nl.length()]; - - for (int i = 0; i != nlBytes.length; i++) { - nlBytes[i] = (byte) nl.charAt(i); - } - - return nlBytes; - } - - private static int getLengthWithoutSeparatorOrTrailingWhitespace(byte[] line) { - int end = line.length - 1; - - while (end >= 0 && isWhiteSpace(line[end])) { - end--; - } - - return end + 1; - } - - private static boolean isLineEnding(byte b) { - return b == '\r' || b == '\n'; - } - - private static boolean isWhiteSpace(byte b) { - return isLineEnding(b) || b == '\t' || b == ' '; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/CleartextSignatureProcessor.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/CleartextSignatureProcessor.java deleted file mode 100644 index 636b78e4..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/CleartextSignatureProcessor.java +++ /dev/null @@ -1,81 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification.cleartext_signatures; - -import java.io.IOException; -import java.io.InputStream; -import java.util.logging.Logger; - -import org.bouncycastle.bcpg.ArmoredInputStream; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.PGPSignatureList; -import org.pgpainless.PGPainless; -import org.pgpainless.algorithm.CompressionAlgorithm; -import org.pgpainless.algorithm.StreamEncoding; -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; -import org.pgpainless.decryption_verification.ConsumerOptions; -import org.pgpainless.decryption_verification.DecryptionStream; -import org.pgpainless.decryption_verification.OpenPgpMetadata; -import org.pgpainless.exception.SignatureValidationException; -import org.pgpainless.util.ArmoredInputStreamFactory; - -/** - * Processor for cleartext-signed messages. - */ -public class CleartextSignatureProcessor { - - private static final Logger LOGGER = Logger.getLogger(CleartextSignatureProcessor.class.getName()); - - private final ArmoredInputStream in; - private final ConsumerOptions options; - private final MultiPassStrategy multiPassStrategy; - - public CleartextSignatureProcessor(InputStream inputStream, - ConsumerOptions options, - MultiPassStrategy multiPassStrategy) - throws IOException { - if (inputStream instanceof ArmoredInputStream) { - this.in = (ArmoredInputStream) inputStream; - } else { - this.in = ArmoredInputStreamFactory.get(inputStream); - } - this.options = options; - this.multiPassStrategy = multiPassStrategy; - } - - /** - * Perform the first pass of cleartext signed message processing: - * Unpack the message from the ascii armor and detach signatures. - * The plaintext message is being written to cache/disk according to the used {@link MultiPassStrategy}. - * - * The result of this method is a {@link DecryptionStream} which will perform the second pass. - * It again outputs the plaintext message and performs signature verification. - * - * The result of {@link DecryptionStream#getResult()} contains information about the messages signatures. - * - * @return validated signature - * @throws IOException if the signature cannot be read. - * @throws PGPException if the signature cannot be initialized. - * @throws SignatureValidationException if the signature is invalid. - */ - public DecryptionStream getVerificationStream() throws IOException, PGPException { - OpenPgpMetadata.Builder resultBuilder = OpenPgpMetadata.getBuilder(); - resultBuilder.setCompressionAlgorithm(CompressionAlgorithm.UNCOMPRESSED) - .setSymmetricKeyAlgorithm(SymmetricKeyAlgorithm.NULL) - .setFileEncoding(StreamEncoding.TEXT); - - PGPSignatureList signatures = ClearsignedMessageUtil.detachSignaturesFromInbandClearsignedMessage(in, multiPassStrategy.getMessageOutputStream()); - - for (PGPSignature signature : signatures) { - options.addVerificationOfDetachedSignature(signature); - } - - return PGPainless.decryptAndOrVerify() - .onInputStream(multiPassStrategy.getMessageInputStream()) - .withOptions(options); - } - -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/InMemoryMultiPassStrategy.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/InMemoryMultiPassStrategy.java deleted file mode 100644 index 62433a2a..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/InMemoryMultiPassStrategy.java +++ /dev/null @@ -1,35 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification.cleartext_signatures; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; - -/** - * Implementation of the {@link MultiPassStrategy}. - * This class keeps the read data in memory by caching the data inside a {@link ByteArrayOutputStream}. - * - * Note, that this class is suitable and efficient for processing small amounts of data. - * For larger data like encrypted files, use of the {@link WriteToFileMultiPassStrategy} is recommended to - * prevent {@link OutOfMemoryError OutOfMemoryErrors} and other issues. - */ -public class InMemoryMultiPassStrategy implements MultiPassStrategy { - - private final ByteArrayOutputStream cache = new ByteArrayOutputStream(); - - @Override - public ByteArrayOutputStream getMessageOutputStream() { - return cache; - } - - @Override - public ByteArrayInputStream getMessageInputStream() { - return new ByteArrayInputStream(getBytes()); - } - - public byte[] getBytes() { - return getMessageOutputStream().toByteArray(); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/MultiPassStrategy.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/MultiPassStrategy.java deleted file mode 100644 index ab5781ca..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/MultiPassStrategy.java +++ /dev/null @@ -1,70 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification.cleartext_signatures; - -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -/** - * Since the {@link CleartextSignatureProcessor} needs to read the whole data twice in order to verify signatures, - * a strategy for how to cache the read data is required. - * Otherwise large data kept in memory could cause {@link OutOfMemoryError OutOfMemoryErrors} or other issues. - * - * This is an Interface that describes a strategy to deal with the fact that detached signatures require multiple passes - * to do verification. - * - * This interface can be used to write the signed data stream out via {@link #getMessageOutputStream()} and later - * get access to the data again via {@link #getMessageInputStream()}. - * Thereby the detail where the data is being stored (memory, file, etc.) can be abstracted away. - */ -public interface MultiPassStrategy { - - /** - * Provide an {@link OutputStream} into which the signed data can be read into. - * - * @return output stream - * @throws IOException io error - */ - OutputStream getMessageOutputStream() throws IOException; - - /** - * Provide an {@link InputStream} which contains the data that was previously written away in - * {@link #getMessageOutputStream()}. - * - * As there may be multiple signatures that need to be processed, each call of this method MUST return - * a new {@link InputStream}. - * - * @return input stream - * @throws IOException io error - */ - InputStream getMessageInputStream() throws IOException; - - /** - * Write the message content out to a file and re-read it to verify signatures. - * This strategy is best suited for larger messages (eg. plaintext signed files) which might not fit into memory. - * After the message has been processed completely, the messages content are available at the provided file. - * - * @param file target file - * @return strategy - */ - static MultiPassStrategy writeMessageToFile(File file) { - return new WriteToFileMultiPassStrategy(file); - } - - /** - * Read the message content into memory. - * This strategy is best suited for small messages which fit into memory. - * After the message has been processed completely, the message content can be accessed by calling - * {@link ByteArrayOutputStream#toByteArray()} on {@link #getMessageOutputStream()}. - * - * @return strategy - */ - static InMemoryMultiPassStrategy keepMessageInMemory() { - return new InMemoryMultiPassStrategy(); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/VerifyCleartextSignatures.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/VerifyCleartextSignatures.java deleted file mode 100644 index 31317c6c..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/VerifyCleartextSignatures.java +++ /dev/null @@ -1,53 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification.cleartext_signatures; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; - -import org.pgpainless.decryption_verification.ConsumerOptions; - -/** - * Interface defining the API for verification of cleartext signed documents. - */ -public interface VerifyCleartextSignatures { - - /** - * Provide the {@link InputStream} which contains the cleartext-signed message. - * @param inputStream inputstream - * @return api handle - */ - WithStrategy onInputStream(InputStream inputStream); - - interface WithStrategy { - - /** - * Provide a {@link MultiPassStrategy} which is used to store the message content. - * Since cleartext-signed messages cannot be processed in one pass, the message has to be passed twice. - * Therefore the user needs to decide upon a strategy where to cache/store the message between the passes. - * This could be {@link MultiPassStrategy#writeMessageToFile(File)} or {@link MultiPassStrategy#keepMessageInMemory()}, - * depending on message size and use-case. - * - * @param multiPassStrategy strategy - * @return api handle - */ - VerifyWith withStrategy(MultiPassStrategy multiPassStrategy); - - } - - interface VerifyWith { - - /** - * Pass in consumer options like verification certificates, acceptable date ranges etc. - * - * @param options options - * @return processor - * @throws IOException in case of an IO error - */ - CleartextSignatureProcessor withOptions(ConsumerOptions options) throws IOException; - - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/VerifyCleartextSignaturesImpl.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/VerifyCleartextSignaturesImpl.java deleted file mode 100644 index 276e027f..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/VerifyCleartextSignaturesImpl.java +++ /dev/null @@ -1,43 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification.cleartext_signatures; - -import java.io.IOException; -import java.io.InputStream; - -import org.pgpainless.decryption_verification.ConsumerOptions; - -public class VerifyCleartextSignaturesImpl implements VerifyCleartextSignatures { - - private InputStream inputStream; - private MultiPassStrategy multiPassStrategy; - - @Override - public WithStrategy onInputStream(InputStream inputStream) { - VerifyCleartextSignaturesImpl.this.inputStream = inputStream; - return new WithStrategyImpl(); - } - - public class WithStrategyImpl implements WithStrategy { - - @Override - public VerifyWith withStrategy(MultiPassStrategy multiPassStrategy) { - if (multiPassStrategy == null) { - throw new NullPointerException("MultiPassStrategy cannot be null."); - } - VerifyCleartextSignaturesImpl.this.multiPassStrategy = multiPassStrategy; - return new VerifyWithImpl(); - } - } - - public class VerifyWithImpl implements VerifyWith { - - @Override - public CleartextSignatureProcessor withOptions(ConsumerOptions options) throws IOException { - return new CleartextSignatureProcessor(inputStream, options, multiPassStrategy); - } - - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/WriteToFileMultiPassStrategy.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/WriteToFileMultiPassStrategy.java deleted file mode 100644 index 6c8d03ca..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/WriteToFileMultiPassStrategy.java +++ /dev/null @@ -1,54 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification.cleartext_signatures; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -/** - * Implementation of the {@link MultiPassStrategy}. - * When processing signed data the first time, the data is being written out into a file. - * For the second pass, that file is being read again. - * - * This strategy is recommended when larger amounts of data need to be processed. - * For smaller files, {@link InMemoryMultiPassStrategy} yields higher efficiency. - */ -public class WriteToFileMultiPassStrategy implements MultiPassStrategy { - - private final File file; - - /** - * Create a {@link MultiPassStrategy} which writes data to a file. - * Note that {@link #getMessageOutputStream()} will create the file if necessary. - * - * @param file file to write the data to and read from - */ - public WriteToFileMultiPassStrategy(File file) { - this.file = file; - } - - @Override - public OutputStream getMessageOutputStream() throws IOException { - if (!file.exists()) { - boolean created = file.createNewFile(); - if (!created) { - throw new IOException("New file '" + file.getAbsolutePath() + "' was not created."); - } - } - return new FileOutputStream(file); - } - - @Override - public InputStream getMessageInputStream() throws IOException { - if (!file.exists()) { - throw new IOException("File '" + file.getAbsolutePath() + "' does no longer exist."); - } - return new FileInputStream(file); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/package-info.java deleted file mode 100644 index 3123338f..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Classes related to cleartext signature verification. - */ -package org.pgpainless.decryption_verification.cleartext_signatures; diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionBuilder.java deleted file mode 100644 index 9490551b..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionBuilder.java +++ /dev/null @@ -1,77 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.encryption_signing; - -import java.io.IOException; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.List; -import java.util.Set; -import javax.annotation.Nonnull; - -import org.bouncycastle.openpgp.PGPException; -import org.pgpainless.PGPainless; -import org.pgpainless.algorithm.CompressionAlgorithm; -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; -import org.pgpainless.algorithm.negotiation.SymmetricKeyAlgorithmNegotiator; -import org.pgpainless.key.SubkeyIdentifier; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class EncryptionBuilder implements EncryptionBuilderInterface { - - private static final Logger LOGGER = LoggerFactory.getLogger(EncryptionBuilder.class); - - private OutputStream outputStream; - - @Override - public WithOptions onOutputStream(@Nonnull OutputStream outputStream) { - this.outputStream = outputStream; - return new WithOptionsImpl(); - } - - class WithOptionsImpl implements WithOptions { - @Override - public EncryptionStream withOptions(ProducerOptions options) throws PGPException, IOException { - if (options == null) { - throw new NullPointerException("ProducerOptions cannot be null."); - } - return new EncryptionStream(outputStream, options); - } - } - - /** - * Negotiate the {@link SymmetricKeyAlgorithm} used for message encryption. - * - * @param encryptionOptions encryption options - * @return negotiated symmetric key algorithm - */ - public static SymmetricKeyAlgorithm negotiateSymmetricEncryptionAlgorithm(EncryptionOptions encryptionOptions) { - List> preferences = new ArrayList<>(); - for (SubkeyIdentifier key : encryptionOptions.getKeyViews().keySet()) { - preferences.add(encryptionOptions.getKeyViews().get(key).getPreferredSymmetricKeyAlgorithms()); - } - - SymmetricKeyAlgorithm algorithm = SymmetricKeyAlgorithmNegotiator - .byPopularity() - .negotiate( - PGPainless.getPolicy().getSymmetricKeyEncryptionAlgorithmPolicy(), - encryptionOptions.getEncryptionAlgorithmOverride(), - preferences); - LOGGER.debug("Negotiation resulted in {} being the symmetric encryption algorithm of choice.", algorithm); - return algorithm; - } - - public static CompressionAlgorithm negotiateCompressionAlgorithm(ProducerOptions producerOptions) { - CompressionAlgorithm compressionAlgorithmOverride = producerOptions.getCompressionAlgorithmOverride(); - if (compressionAlgorithmOverride != null) { - return compressionAlgorithmOverride; - } - - // TODO: Negotiation - - return PGPainless.getPolicy().getCompressionAlgorithmPolicy().defaultCompressionAlgorithm(); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionBuilderInterface.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionBuilderInterface.java deleted file mode 100644 index 9ba0bb78..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionBuilderInterface.java +++ /dev/null @@ -1,35 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.encryption_signing; - -import java.io.IOException; -import java.io.OutputStream; -import javax.annotation.Nonnull; - -import org.bouncycastle.openpgp.PGPException; - -public interface EncryptionBuilderInterface { - - /** - * Create a {@link EncryptionStream} on an {@link OutputStream} that contains the plain data that - * shall be encrypted and or signed. - * - * @param outputStream output stream of the plain data. - * @return api handle - */ - WithOptions onOutputStream(@Nonnull OutputStream outputStream); - - interface WithOptions { - - /** - * Create an {@link EncryptionStream} with the given options (recipients, signers, algorithms...). - * - * @param options options - * @return encryption strea - */ - EncryptionStream withOptions(ProducerOptions options) throws PGPException, IOException; - - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java deleted file mode 100644 index befced35..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java +++ /dev/null @@ -1,316 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.encryption_signing; - -import java.util.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import javax.annotation.Nonnull; - -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; -import org.bouncycastle.openpgp.operator.PBEKeyEncryptionMethodGenerator; -import org.bouncycastle.openpgp.operator.PGPKeyEncryptionMethodGenerator; -import org.pgpainless.algorithm.EncryptionPurpose; -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; -import org.pgpainless.implementation.ImplementationFactory; -import org.pgpainless.key.OpenPgpV4Fingerprint; -import org.pgpainless.key.SubkeyIdentifier; -import org.pgpainless.key.info.KeyAccessor; -import org.pgpainless.key.info.KeyRingInfo; -import org.pgpainless.util.Passphrase; - -/** - * Options for the encryption process. - * This class can be used to set encryption parameters, like encryption keys and passphrases, algorithms etc. - * - * A typical use might look like follows: - *
- * {@code
- * EncryptionOptions opt = new EncryptionOptions();
- * opt.addRecipient(aliceKey, "Alice ");
- * opt.addPassphrase(Passphrase.fromPassword("AdditionalDecryptionPassphrase123"));
- * }
- * 
- * - * To use a custom symmetric encryption algorithm, use {@link #overrideEncryptionAlgorithm(SymmetricKeyAlgorithm)}. - * This will cause PGPainless to use the provided algorithm for message encryption, instead of negotiating an algorithm - * by inspecting the provided recipient keys. - * - * By default, PGPainless will only encrypt to a single encryption capable subkey per recipient key. - * This behavior can be changed, eg. by calling - *
- * {@code
- * opt.addRecipient(aliceKey, EncryptionOptions.encryptToAllCapableSubkeys());
- * }
- * 
- * when adding the recipient key. - */ -public class EncryptionOptions { - - private final EncryptionPurpose purpose; - private final Set encryptionMethods = new LinkedHashSet<>(); - private final Set encryptionKeys = new LinkedHashSet<>(); - private final Map keyRingInfo = new HashMap<>(); - private final Map keyViews = new HashMap<>(); - private final EncryptionKeySelector encryptionKeySelector = encryptToAllCapableSubkeys(); - - private SymmetricKeyAlgorithm encryptionAlgorithmOverride = null; - - /** - * Encrypt to keys both carrying the key flag {@link org.pgpainless.algorithm.KeyFlag#ENCRYPT_COMMS} - * or {@link org.pgpainless.algorithm.KeyFlag#ENCRYPT_STORAGE}. - */ - public EncryptionOptions() { - this(EncryptionPurpose.STORAGE_AND_COMMUNICATIONS); - } - - public EncryptionOptions(EncryptionPurpose purpose) { - this.purpose = purpose; - } - - /** - * Factory method to create an {@link EncryptionOptions} object which will encrypt for keys - * which carry the flag {@link org.pgpainless.algorithm.KeyFlag#ENCRYPT_COMMS}. - * - * @return encryption options - */ - public static EncryptionOptions encryptCommunications() { - return new EncryptionOptions(EncryptionPurpose.COMMUNICATIONS); - } - - /** - * Factory method to create an {@link EncryptionOptions} object which will encrypt for keys - * which carry the flag {@link org.pgpainless.algorithm.KeyFlag#ENCRYPT_STORAGE}. - * - * @return encryption options - */ - public static EncryptionOptions encryptDataAtRest() { - return new EncryptionOptions(EncryptionPurpose.STORAGE); - } - - /** - * Add all key rings in the provided key ring collection as recipients. - * - * @param keys keys - * @return this - */ - public EncryptionOptions addRecipients(PGPPublicKeyRingCollection keys) { - for (PGPPublicKeyRing key : keys) { - addRecipient(key); - } - return this; - } - - /** - * Add all key rings in the provided key ring collection as recipients. - * Per key ring, the selector is applied to select one or more encryption subkeys. - * - * @param keys keys - * @param selector encryption key selector - * @return this - */ - public EncryptionOptions addRecipients(@Nonnull PGPPublicKeyRingCollection keys, @Nonnull EncryptionKeySelector selector) { - for (PGPPublicKeyRing key : keys) { - addRecipient(key, selector); - } - return this; - } - - /** - * Add a recipient by providing a key and recipient user-id. - * The user-id is used to determine the recipients preferences (algorithms etc.). - * - * @param key key ring - * @param userId user id - * @return this - */ - public EncryptionOptions addRecipient(PGPPublicKeyRing key, String userId) { - return addRecipient(key, userId, encryptionKeySelector); - } - - /** - * Add a recipient by providing a key and recipient user-id, as well as a strategy for selecting one or multiple - * encryption capable subkeys from the key. - * - * @param key key - * @param userId user-id - * @param encryptionKeySelectionStrategy strategy to select one or more encryption subkeys to encrypt to - * @return this - */ - public EncryptionOptions addRecipient(PGPPublicKeyRing key, String userId, EncryptionKeySelector encryptionKeySelectionStrategy) { - KeyRingInfo info = new KeyRingInfo(key, new Date()); - - List encryptionSubkeys = encryptionKeySelectionStrategy - .selectEncryptionSubkeys(info.getEncryptionSubkeys(userId, purpose)); - if (encryptionSubkeys.isEmpty()) { - throw new IllegalArgumentException("Key has no suitable encryption subkeys."); - } - - for (PGPPublicKey encryptionSubkey : encryptionSubkeys) { - SubkeyIdentifier keyId = new SubkeyIdentifier(key, encryptionSubkey.getKeyID()); - keyRingInfo.put(keyId, info); - keyViews.put(keyId, new KeyAccessor.ViaUserId(info, keyId, userId)); - addRecipientKey(key, encryptionSubkey); - } - - return this; - } - - /** - * Add a recipient by providing a key. - * - * @param key key ring - * @return this - */ - public EncryptionOptions addRecipient(PGPPublicKeyRing key) { - return addRecipient(key, encryptionKeySelector); - } - - /** - * Add a recipient by providing a key and an encryption key selection strategy. - * - * @param key key ring - * @param encryptionKeySelectionStrategy strategy used to select one or multiple encryption subkeys. - * @return this - */ - public EncryptionOptions addRecipient(PGPPublicKeyRing key, EncryptionKeySelector encryptionKeySelectionStrategy) { - KeyRingInfo info = new KeyRingInfo(key, new Date()); - Date primaryKeyExpiration = info.getPrimaryKeyExpirationDate(); - if (primaryKeyExpiration != null && primaryKeyExpiration.before(new Date())) { - throw new IllegalArgumentException("Provided key " + new OpenPgpV4Fingerprint(key) + " is expired: " + primaryKeyExpiration.toString()); - } - List encryptionSubkeys = encryptionKeySelectionStrategy - .selectEncryptionSubkeys(info.getEncryptionSubkeys(purpose)); - if (encryptionSubkeys.isEmpty()) { - throw new IllegalArgumentException("Key has no suitable encryption subkeys."); - } - - for (PGPPublicKey encryptionSubkey : encryptionSubkeys) { - SubkeyIdentifier keyId = new SubkeyIdentifier(key, encryptionSubkey.getKeyID()); - keyRingInfo.put(keyId, info); - keyViews.put(keyId, new KeyAccessor.ViaKeyId(info, keyId)); - addRecipientKey(key, encryptionSubkey); - } - - return this; - } - - private void addRecipientKey(PGPPublicKeyRing keyRing, PGPPublicKey key) { - encryptionKeys.add(new SubkeyIdentifier(keyRing, key.getKeyID())); - PGPKeyEncryptionMethodGenerator encryptionMethod = ImplementationFactory - .getInstance().getPublicKeyKeyEncryptionMethodGenerator(key); - addEncryptionMethod(encryptionMethod); - } - - /** - * Add a symmetric passphrase which the message will be encrypted to. - * - * @param passphrase passphrase - * @return this - */ - public EncryptionOptions addPassphrase(Passphrase passphrase) { - if (passphrase.isEmpty()) { - throw new IllegalArgumentException("Passphrase must not be empty."); - } - PBEKeyEncryptionMethodGenerator encryptionMethod = ImplementationFactory - .getInstance().getPBEKeyEncryptionMethodGenerator(passphrase); - return addEncryptionMethod(encryptionMethod); - } - - /** - * Add an {@link PGPKeyEncryptionMethodGenerator} which will be used to encrypt the message. - * Method generators are either {@link PBEKeyEncryptionMethodGenerator} (passphrase) - * or {@link PGPKeyEncryptionMethodGenerator} (public key). - * - * This method is intended for advanced users to allow encryption for specific subkeys. - * This can come in handy for example if data needs to be encrypted to a subkey that's ignored by PGPainless. - * - * @param encryptionMethod encryption method - * @return this - */ - public EncryptionOptions addEncryptionMethod(PGPKeyEncryptionMethodGenerator encryptionMethod) { - encryptionMethods.add(encryptionMethod); - return this; - } - - Set getEncryptionMethods() { - return new HashSet<>(encryptionMethods); - } - - Map getKeyRingInfo() { - return new HashMap<>(keyRingInfo); - } - - Set getEncryptionKeyIdentifiers() { - return new HashSet<>(encryptionKeys); - } - - Map getKeyViews() { - return new HashMap<>(keyViews); - } - - SymmetricKeyAlgorithm getEncryptionAlgorithmOverride() { - return encryptionAlgorithmOverride; - } - - /** - * Override the used symmetric encryption algorithm. - * The symmetric encryption algorithm is used to encrypt the message itself, - * while the used symmetric key will be encrypted to all recipients using public key - * cryptography. - * - * If the algorithm is not overridden, a suitable algorithm will be negotiated. - * - * @param encryptionAlgorithm encryption algorithm override - */ - public void overrideEncryptionAlgorithm(SymmetricKeyAlgorithm encryptionAlgorithm) { - if (encryptionAlgorithm == SymmetricKeyAlgorithm.NULL) { - throw new IllegalArgumentException("Plaintext encryption can only be used to denote unencrypted secret keys."); - } - this.encryptionAlgorithmOverride = encryptionAlgorithm; - } - - public interface EncryptionKeySelector { - List selectEncryptionSubkeys(List encryptionCapableKeys); - } - - /** - * Only encrypt to the first valid encryption capable subkey we stumble upon. - * - * @return encryption key selector - */ - public static EncryptionKeySelector encryptToFirstSubkey() { - return new EncryptionKeySelector() { - @Override - public List selectEncryptionSubkeys(List encryptionCapableKeys) { - return encryptionCapableKeys.isEmpty() ? Collections.emptyList() : Collections.singletonList(encryptionCapableKeys.get(0)); - } - }; - } - - /** - * Encrypt to any valid, encryption capable subkey on the key ring. - * - * @return encryption key selector - */ - public static EncryptionKeySelector encryptToAllCapableSubkeys() { - return new EncryptionKeySelector() { - @Override - public List selectEncryptionSubkeys(List encryptionCapableKeys) { - return encryptionCapableKeys; - } - }; - } - - // TODO: Create encryptToBestSubkey() method -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionResult.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionResult.java deleted file mode 100644 index 21d42254..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionResult.java +++ /dev/null @@ -1,200 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.encryption_signing; - -import java.util.Collections; -import java.util.Date; -import java.util.HashSet; -import java.util.Set; -import javax.annotation.Nonnull; - -import org.bouncycastle.openpgp.PGPLiteralData; -import org.bouncycastle.openpgp.PGPSignature; -import org.pgpainless.algorithm.CompressionAlgorithm; -import org.pgpainless.algorithm.StreamEncoding; -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; -import org.pgpainless.key.SubkeyIdentifier; -import org.pgpainless.util.MultiMap; - -public final class EncryptionResult { - - private final SymmetricKeyAlgorithm encryptionAlgorithm; - private final CompressionAlgorithm compressionAlgorithm; - - private final MultiMap detachedSignatures; - private final Set recipients; - private final String fileName; - private final Date modificationDate; - private final StreamEncoding fileEncoding; - - private EncryptionResult(SymmetricKeyAlgorithm encryptionAlgorithm, - CompressionAlgorithm compressionAlgorithm, - MultiMap detachedSignatures, - Set recipients, - String fileName, - Date modificationDate, - StreamEncoding encoding) { - this.encryptionAlgorithm = encryptionAlgorithm; - this.compressionAlgorithm = compressionAlgorithm; - this.detachedSignatures = detachedSignatures; - this.recipients = Collections.unmodifiableSet(recipients); - this.fileName = fileName; - this.modificationDate = modificationDate; - this.fileEncoding = encoding; - } - - /** - * Return the symmetric encryption algorithm used to encrypt the message. - * @return symmetric encryption algorithm - * - * @deprecated use {@link #getEncryptionAlgorithm()} instead. - */ - @Deprecated - public SymmetricKeyAlgorithm getSymmetricKeyAlgorithm() { - return getEncryptionAlgorithm(); - } - - /** - * Return the symmetric encryption algorithm used to encrypt the message. - * - * @return symmetric encryption algorithm - * */ - public SymmetricKeyAlgorithm getEncryptionAlgorithm() { - return encryptionAlgorithm; - } - - /** - * Return the compression algorithm that was used to compress the message before encryption/signing. - * - * @return compression algorithm - */ - public CompressionAlgorithm getCompressionAlgorithm() { - return compressionAlgorithm; - } - - /** - * Return a {@link MultiMap} of key identifiers and detached signatures that were generated for the message. - * Each key of the map represents a signing key, which has one or more detached signatures associated with it. - * - * @return detached signatures - */ - public MultiMap getDetachedSignatures() { - return detachedSignatures; - } - - /** - * Return the set of recipient encryption keys. - * - * @return recipients - */ - public Set getRecipients() { - return recipients; - } - - /** - * Return the file name of the encrypted/signed data. - * - * @return filename - */ - public String getFileName() { - return fileName; - } - - /** - * Return the modification date of the encrypted/signed file. - * - * @return modification date - */ - public Date getModificationDate() { - return modificationDate; - } - - /** - * Return the encoding format of the encrypted/signed data. - * - * @return encoding format - */ - public StreamEncoding getFileEncoding() { - return fileEncoding; - } - - /** - * Return true, if the message is marked as for-your-eyes-only. - * This is typically done by setting the filename "_CONSOLE". - * - * @return is message for your eyes only? - */ - public boolean isForYourEyesOnly() { - return PGPLiteralData.CONSOLE.equals(getFileName()); - } - - /** - * Create a builder for the encryption result class. - * - * @return builder - */ - public static Builder builder() { - return new Builder(); - } - - public static class Builder { - - private SymmetricKeyAlgorithm encryptionAlgorithm; - private CompressionAlgorithm compressionAlgorithm; - - private final MultiMap detachedSignatures = new MultiMap<>(); - private Set recipients = new HashSet<>(); - private String fileName = ""; - private Date modificationDate = new Date(0L); // NOW - private StreamEncoding encoding = StreamEncoding.BINARY; - - public Builder setEncryptionAlgorithm(SymmetricKeyAlgorithm encryptionAlgorithm) { - this.encryptionAlgorithm = encryptionAlgorithm; - return this; - } - - public Builder setCompressionAlgorithm(CompressionAlgorithm compressionAlgorithm) { - this.compressionAlgorithm = compressionAlgorithm; - return this; - } - - public Builder addRecipient(SubkeyIdentifier recipient) { - this.recipients.add(recipient); - return this; - } - - public Builder addDetachedSignature(SubkeyIdentifier signingSubkeyIdentifier, PGPSignature detachedSignature) { - this.detachedSignatures.put(signingSubkeyIdentifier, detachedSignature); - return this; - } - - public Builder setFileName(@Nonnull String fileName) { - this.fileName = fileName; - return this; - } - - public Builder setModificationDate(@Nonnull Date modificationDate) { - this.modificationDate = modificationDate; - return this; - } - - public Builder setFileEncoding(StreamEncoding fileEncoding) { - this.encoding = fileEncoding; - return this; - } - - public EncryptionResult build() { - if (encryptionAlgorithm == null) { - throw new IllegalStateException("Encryption algorithm not set."); - } - if (compressionAlgorithm == null) { - throw new IllegalStateException("Compression algorithm not set."); - } - - return new EncryptionResult(encryptionAlgorithm, compressionAlgorithm, detachedSignatures, recipients, - fileName, modificationDate, encoding); - } - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java deleted file mode 100644 index 7d2b9722..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java +++ /dev/null @@ -1,287 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.encryption_signing; - -import java.io.IOException; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.List; -import javax.annotation.Nonnull; - -import org.bouncycastle.bcpg.ArmoredOutputStream; -import org.bouncycastle.bcpg.BCPGOutputStream; -import org.bouncycastle.openpgp.PGPCompressedDataGenerator; -import org.bouncycastle.openpgp.PGPEncryptedDataGenerator; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPLiteralDataGenerator; -import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.PGPSignatureGenerator; -import org.bouncycastle.openpgp.operator.PGPDataEncryptorBuilder; -import org.bouncycastle.openpgp.operator.PGPKeyEncryptionMethodGenerator; -import org.pgpainless.algorithm.CompressionAlgorithm; -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; -import org.pgpainless.implementation.ImplementationFactory; -import org.pgpainless.key.SubkeyIdentifier; -import org.pgpainless.util.ArmoredOutputStreamFactory; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * This class is based upon Jens Neuhalfen's Bouncy-GPG PGPEncryptingStream. - * @see Source - */ -public final class EncryptionStream extends OutputStream { - - private static final Logger LOGGER = LoggerFactory.getLogger(EncryptionStream.class); - - private final ProducerOptions options; - private final EncryptionResult.Builder resultBuilder = EncryptionResult.builder(); - - private boolean closed = false; - // 1 << 8 causes wrong partial body length encoding - // 1 << 9 fixes this. - // see https://github.com/pgpainless/pgpainless/issues/160 - private static final int BUFFER_SIZE = 1 << 9; - - OutputStream outermostStream; - private ArmoredOutputStream armorOutputStream = null; - private OutputStream publicKeyEncryptedStream = null; - private PGPCompressedDataGenerator compressedDataGenerator; - private BCPGOutputStream basicCompressionStream; - private PGPLiteralDataGenerator literalDataGenerator; - private OutputStream literalDataStream; - - EncryptionStream(@Nonnull OutputStream targetOutputStream, - @Nonnull ProducerOptions options) - throws IOException, PGPException { - this.options = options; - outermostStream = targetOutputStream; - - prepareArmor(); - prepareEncryption(); - prepareCompression(); - prepareOnePassSignatures(); - prepareLiteralDataProcessing(); - } - - private void prepareArmor() { - if (!options.isAsciiArmor()) { - LOGGER.debug("Output will be unarmored"); - return; - } - - LOGGER.debug("Wrap encryption output in ASCII armor"); - armorOutputStream = ArmoredOutputStreamFactory.get(outermostStream); - outermostStream = armorOutputStream; - } - - private void prepareEncryption() throws IOException, PGPException { - EncryptionOptions encryptionOptions = options.getEncryptionOptions(); - if (encryptionOptions == null || encryptionOptions.getEncryptionMethods().isEmpty()) { - // No encryption options/methods -> no encryption - resultBuilder.setEncryptionAlgorithm(SymmetricKeyAlgorithm.NULL); - return; - } - - SymmetricKeyAlgorithm encryptionAlgorithm = EncryptionBuilder.negotiateSymmetricEncryptionAlgorithm(encryptionOptions); - resultBuilder.setEncryptionAlgorithm(encryptionAlgorithm); - LOGGER.debug("Encrypt message using {}", encryptionAlgorithm); - PGPDataEncryptorBuilder dataEncryptorBuilder = - ImplementationFactory.getInstance().getPGPDataEncryptorBuilder(encryptionAlgorithm); - dataEncryptorBuilder.setWithIntegrityPacket(true); - - PGPEncryptedDataGenerator encryptedDataGenerator = - new PGPEncryptedDataGenerator(dataEncryptorBuilder); - for (PGPKeyEncryptionMethodGenerator encryptionMethod : encryptionOptions.getEncryptionMethods()) { - encryptedDataGenerator.addMethod(encryptionMethod); - } - - for (SubkeyIdentifier recipientSubkeyIdentifier : encryptionOptions.getEncryptionKeyIdentifiers()) { - resultBuilder.addRecipient(recipientSubkeyIdentifier); - } - - publicKeyEncryptedStream = encryptedDataGenerator.open(outermostStream, new byte[BUFFER_SIZE]); - outermostStream = publicKeyEncryptedStream; - } - - private void prepareCompression() throws IOException { - CompressionAlgorithm compressionAlgorithm = EncryptionBuilder.negotiateCompressionAlgorithm(options); - resultBuilder.setCompressionAlgorithm(compressionAlgorithm); - compressedDataGenerator = new PGPCompressedDataGenerator( - compressionAlgorithm.getAlgorithmId()); - if (compressionAlgorithm == CompressionAlgorithm.UNCOMPRESSED) { - return; - } - - LOGGER.debug("Compress using {}", compressionAlgorithm); - basicCompressionStream = new BCPGOutputStream(compressedDataGenerator.open(outermostStream)); - outermostStream = basicCompressionStream; - } - - private void prepareOnePassSignatures() throws IOException, PGPException { - SigningOptions signingOptions = options.getSigningOptions(); - if (signingOptions == null || signingOptions.getSigningMethods().isEmpty()) { - // No singing options/methods -> no signing - return; - } - - int sigIndex = 0; - for (SubkeyIdentifier identifier : signingOptions.getSigningMethods().keySet()) { - sigIndex++; - SigningOptions.SigningMethod signingMethod = signingOptions.getSigningMethods().get(identifier); - - if (!signingMethod.isDetached()) { - PGPSignatureGenerator signatureGenerator = signingMethod.getSignatureGenerator(); - // The last sig is not nested, all others are - boolean nested = sigIndex != signingOptions.getSigningMethods().size(); - signatureGenerator.generateOnePassVersion(nested).encode(outermostStream); - } - } - } - - private void prepareLiteralDataProcessing() throws IOException { - if (options.isCleartextSigned()) { - SigningOptions.SigningMethod firstMethod = options.getSigningOptions().getSigningMethods().values().iterator().next(); - armorOutputStream.beginClearText(firstMethod.getHashAlgorithm().getAlgorithmId()); - return; - } - literalDataGenerator = new PGPLiteralDataGenerator(); - literalDataStream = literalDataGenerator.open(outermostStream, - options.getEncoding().getCode(), - options.getFileName(), - options.getModificationDate(), - new byte[BUFFER_SIZE]); - outermostStream = literalDataStream; - - resultBuilder.setFileName(options.getFileName()) - .setModificationDate(options.getModificationDate()) - .setFileEncoding(options.getEncoding()); - } - - @Override - public void write(int data) throws IOException { - outermostStream.write(data); - SigningOptions signingOptions = options.getSigningOptions(); - if (signingOptions == null || signingOptions.getSigningMethods().isEmpty()) { - return; - } - - for (SubkeyIdentifier signingKey : signingOptions.getSigningMethods().keySet()) { - SigningOptions.SigningMethod signingMethod = signingOptions.getSigningMethods().get(signingKey); - PGPSignatureGenerator signatureGenerator = signingMethod.getSignatureGenerator(); - byte asByte = (byte) (data & 0xff); - signatureGenerator.update(asByte); - } - } - - @Override - public void write(@Nonnull byte[] buffer) throws IOException { - write(buffer, 0, buffer.length); - } - - - @Override - public void write(@Nonnull byte[] buffer, int off, int len) throws IOException { - outermostStream.write(buffer, 0, len); - SigningOptions signingOptions = options.getSigningOptions(); - if (signingOptions == null || signingOptions.getSigningMethods().isEmpty()) { - return; - } - for (SubkeyIdentifier signingKey : signingOptions.getSigningMethods().keySet()) { - SigningOptions.SigningMethod signingMethod = signingOptions.getSigningMethods().get(signingKey); - PGPSignatureGenerator signatureGenerator = signingMethod.getSignatureGenerator(); - signatureGenerator.update(buffer, 0, len); - } - } - - @Override - public void flush() throws IOException { - outermostStream.flush(); - } - - @Override - public void close() throws IOException { - if (closed) { - return; - } - - // Literal Data - if (literalDataStream != null) { - literalDataStream.flush(); - literalDataStream.close(); - } - if (literalDataGenerator != null) { - literalDataGenerator.close(); - } - - if (options.isCleartextSigned()) { - // Add linebreak between body and signatures - // TODO: We should only add this line if required. - // I.e. if the message already ends with \n, don't add another linebreak. - armorOutputStream.write('\r'); - armorOutputStream.write('\n'); - armorOutputStream.endClearText(); - } - - try { - writeSignatures(); - } catch (PGPException e) { - throw new IOException("Exception while writing signatures.", e); - } - - // Compressed Data - compressedDataGenerator.close(); - - // Public Key Encryption - if (publicKeyEncryptedStream != null) { - publicKeyEncryptedStream.flush(); - publicKeyEncryptedStream.close(); - } - - // Armor - if (armorOutputStream != null) { - armorOutputStream.flush(); - armorOutputStream.close(); - } - closed = true; - } - - private void writeSignatures() throws PGPException, IOException { - SigningOptions signingOptions = options.getSigningOptions(); - if (signingOptions == null || signingOptions.getSigningMethods().isEmpty()) { - return; - } - - // One-Pass-Signatures are bracketed. That means we have to append the signatures in reverse order - // compared to the one-pass-signature packets. - List signingKeys = new ArrayList<>(); - for (SubkeyIdentifier signingKey : signingOptions.getSigningMethods().keySet()) { - signingKeys.add(signingKey); - } - for (int i = signingKeys.size() - 1; i >= 0; i--) { - SubkeyIdentifier signingKey = signingKeys.get(i); - SigningOptions.SigningMethod signingMethod = signingOptions.getSigningMethods().get(signingKey); - PGPSignatureGenerator signatureGenerator = signingMethod.getSignatureGenerator(); - PGPSignature signature = signatureGenerator.generate(); - if (signingMethod.isDetached()) { - resultBuilder.addDetachedSignature(signingKey, signature); - } - if (!signingMethod.isDetached() || options.isCleartextSigned()) { - signature.encode(outermostStream); - } - } - } - - public EncryptionResult getResult() { - if (!closed) { - throw new IllegalStateException("EncryptionStream must be closed before accessing the Result."); - } - return resultBuilder.build(); - } - - public boolean isClosed() { - return closed; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/ProducerOptions.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/ProducerOptions.java deleted file mode 100644 index 91e976a1..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/ProducerOptions.java +++ /dev/null @@ -1,226 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.encryption_signing; - -import java.util.Date; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import org.bouncycastle.openpgp.PGPLiteralData; -import org.pgpainless.PGPainless; -import org.pgpainless.algorithm.CompressionAlgorithm; -import org.pgpainless.algorithm.StreamEncoding; - -public final class ProducerOptions { - - private final EncryptionOptions encryptionOptions; - private final SigningOptions signingOptions; - private String fileName = ""; - private Date modificationDate = PGPLiteralData.NOW; - private StreamEncoding streamEncoding = StreamEncoding.BINARY; - private boolean cleartextSigned = false; - - private CompressionAlgorithm compressionAlgorithmOverride = PGPainless.getPolicy().getCompressionAlgorithmPolicy() - .defaultCompressionAlgorithm(); - private boolean asciiArmor = true; - - private ProducerOptions(EncryptionOptions encryptionOptions, SigningOptions signingOptions) { - this.encryptionOptions = encryptionOptions; - this.signingOptions = signingOptions; - } - - /** - * Sign and encrypt some data. - * - * @param encryptionOptions encryption options - * @param signingOptions signing options - * @return builder - */ - public static ProducerOptions signAndEncrypt(EncryptionOptions encryptionOptions, - SigningOptions signingOptions) { - throwIfNull(encryptionOptions); - throwIfNull(signingOptions); - return new ProducerOptions(encryptionOptions, signingOptions); - } - - /** - * Sign some data without encryption. - * - * @param signingOptions signing options - * @return builder - */ - public static ProducerOptions sign(SigningOptions signingOptions) { - throwIfNull(signingOptions); - return new ProducerOptions(null, signingOptions); - } - - /** - * Encrypt some data without signing. - * - * @param encryptionOptions encryption options - * @return builder - */ - public static ProducerOptions encrypt(EncryptionOptions encryptionOptions) { - throwIfNull(encryptionOptions); - return new ProducerOptions(encryptionOptions, null); - } - - public static ProducerOptions noEncryptionNoSigning() { - return new ProducerOptions(null, null); - } - - private static void throwIfNull(EncryptionOptions encryptionOptions) { - if (encryptionOptions == null) { - throw new NullPointerException("EncryptionOptions cannot be null."); - } - } - - private static void throwIfNull(SigningOptions signingOptions) { - if (signingOptions == null) { - throw new NullPointerException("SigningOptions cannot be null."); - } - } - - /** - * Specify, whether or not the result of the encryption/signing operation shall be ascii armored. - * The default value is true. - * - * @param asciiArmor ascii armor - * @return builder - */ - public ProducerOptions setAsciiArmor(boolean asciiArmor) { - if (cleartextSigned && !asciiArmor) { - throw new IllegalArgumentException("Cleartext signing is enabled. Cannot disable ASCII armoring."); - } - this.asciiArmor = asciiArmor; - return this; - } - - /** - * Return true if the output of the encryption/signing operation shall be ascii armored. - * - * @return ascii armored - */ - public boolean isAsciiArmor() { - return asciiArmor; - } - - public ProducerOptions setCleartextSigned() { - if (signingOptions == null) { - throw new IllegalArgumentException("Signing Options cannot be null if cleartext signing is enabled."); - } - if (encryptionOptions != null) { - throw new IllegalArgumentException("Cannot encode encrypted message as Cleartext Signed."); - } - for (SigningOptions.SigningMethod method : signingOptions.getSigningMethods().values()) { - if (!method.isDetached()) { - throw new IllegalArgumentException("For cleartext signed message, all signatures must be added as detached signatures."); - } - } - cleartextSigned = true; - asciiArmor = true; - compressionAlgorithmOverride = CompressionAlgorithm.UNCOMPRESSED; - return this; - } - - public boolean isCleartextSigned() { - return cleartextSigned; - } - - /** - * Set the name of the encrypted file. - * Note: This option cannot be used simultaneously with {@link #setForYourEyesOnly()}. - * - * @param fileName name of the encrypted file - * @return this - */ - public ProducerOptions setFileName(@Nonnull String fileName) { - this.fileName = fileName; - return this; - } - - /** - * Return the encrypted files name. - * - * @return file name - */ - public String getFileName() { - return fileName; - } - - /** - * Mark the encrypted message as for-your-eyes-only by setting a special file name. - * Note: Therefore this method cannot be used simultaneously with {@link #setFileName(String)}. - * - * @return this - */ - public ProducerOptions setForYourEyesOnly() { - this.fileName = PGPLiteralData.CONSOLE; - return this; - } - - /** - * Set the modification date of the encrypted file. - * - * @param modificationDate Modification date of the encrypted file. - * @return this - */ - public ProducerOptions setModificationDate(@Nonnull Date modificationDate) { - this.modificationDate = modificationDate; - return this; - } - - /** - * Return the modification date of the encrypted file. - * - * @return modification date - */ - public Date getModificationDate() { - return modificationDate; - } - - /** - * Set the format of the literal data packet. - * Defaults to {@link StreamEncoding#BINARY}. - * - * @see RFC4880 §5.9. Literal Data Packet - * - * @param encoding encoding - * @return this - */ - public ProducerOptions setEncoding(@Nonnull StreamEncoding encoding) { - this.streamEncoding = encoding; - return this; - } - - public StreamEncoding getEncoding() { - return streamEncoding; - } - /** - * Override which compression algorithm shall be used. - * - * @param compressionAlgorithm compression algorithm override - * @return builder - */ - public ProducerOptions overrideCompressionAlgorithm(CompressionAlgorithm compressionAlgorithm) { - if (compressionAlgorithm == null) { - throw new NullPointerException("Compression algorithm cannot be null."); - } - this.compressionAlgorithmOverride = compressionAlgorithm; - return this; - } - - public CompressionAlgorithm getCompressionAlgorithmOverride() { - return compressionAlgorithmOverride; - } - - public @Nullable EncryptionOptions getEncryptionOptions() { - return encryptionOptions; - } - - public @Nullable SigningOptions getSigningOptions() { - return signingOptions; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java deleted file mode 100644 index eb50db5d..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java +++ /dev/null @@ -1,355 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.encryption_signing; - -import java.util.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPrivateKey; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; -import org.bouncycastle.openpgp.PGPSignatureGenerator; -import org.bouncycastle.openpgp.PGPSignatureSubpacketGenerator; -import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder; -import org.pgpainless.PGPainless; -import org.pgpainless.algorithm.DocumentSignatureType; -import org.pgpainless.algorithm.HashAlgorithm; -import org.pgpainless.exception.KeyCannotSignException; -import org.pgpainless.exception.KeyValidationError; -import org.pgpainless.implementation.ImplementationFactory; -import org.pgpainless.key.OpenPgpV4Fingerprint; -import org.pgpainless.key.SubkeyIdentifier; -import org.pgpainless.key.info.KeyRingInfo; -import org.pgpainless.key.protection.SecretKeyRingProtector; -import org.pgpainless.key.protection.UnlockSecretKey; -import org.pgpainless.policy.Policy; - -public final class SigningOptions { - - /** - * A method of signing. - */ - public static final class SigningMethod { - private final PGPSignatureGenerator signatureGenerator; - private final boolean detached; - private final HashAlgorithm hashAlgorithm; - - private SigningMethod(PGPSignatureGenerator signatureGenerator, boolean detached, HashAlgorithm hashAlgorithm) { - this.signatureGenerator = signatureGenerator; - this.detached = detached; - this.hashAlgorithm = hashAlgorithm; - } - - /** - * Inline-signature method. - * The resulting signature will be written into the message itself, together with a one-pass-signature packet. - * - * @param signatureGenerator signature generator - * @return inline signing method - */ - public static SigningMethod inlineSignature(PGPSignatureGenerator signatureGenerator, HashAlgorithm hashAlgorithm) { - return new SigningMethod(signatureGenerator, false, hashAlgorithm); - } - - /** - * Detached signing method. - * The resulting signature will not be added to the message, and instead can be distributed separately - * to the signed message. - * - * @param signatureGenerator signature generator - * @return detached signing method - */ - public static SigningMethod detachedSignature(PGPSignatureGenerator signatureGenerator, HashAlgorithm hashAlgorithm) { - return new SigningMethod(signatureGenerator, true, hashAlgorithm); - } - - public boolean isDetached() { - return detached; - } - - public PGPSignatureGenerator getSignatureGenerator() { - return signatureGenerator; - } - - public HashAlgorithm getHashAlgorithm() { - return hashAlgorithm; - } - } - - private final Map signingMethods = new HashMap<>(); - private HashAlgorithm hashAlgorithmOverride; - - public static SigningOptions get() { - return new SigningOptions(); - } - - /** - * Add inline signatures with all secret key rings in the provided secret key ring collection. - * - * @param secrectKeyDecryptor decryptor to unlock the signing secret keys - * @param signingKeys collection of signing keys - * @param signatureType type of signature (binary, canonical text) - * @return this - * @throws KeyValidationError if something is wrong with any of the keys - * @throws PGPException if any of the keys cannot be unlocked or a signing method cannot be created - */ - public SigningOptions addInlineSignatures(SecretKeyRingProtector secrectKeyDecryptor, - PGPSecretKeyRingCollection signingKeys, - DocumentSignatureType signatureType) - throws KeyValidationError, PGPException { - for (PGPSecretKeyRing signingKey : signingKeys) { - addInlineSignature(secrectKeyDecryptor, signingKey, signatureType); - } - return this; - } - - /** - * Add an inline-signature. - * Inline signatures are being embedded into the message itself and can be processed in one pass, thanks to the use - * of one-pass-signature packets. - * - * @param secretKeyDecryptor decryptor to unlock the signing secret key - * @param secretKey signing key - * @param signatureType type of signature (binary, canonical text) - * @throws KeyValidationError if something is wrong with the key - * @throws PGPException if the key cannot be unlocked or the signing method cannot be created - * @return this - */ - public SigningOptions addInlineSignature(SecretKeyRingProtector secretKeyDecryptor, - PGPSecretKeyRing secretKey, - DocumentSignatureType signatureType) - throws KeyValidationError, PGPException { - return addInlineSignature(secretKeyDecryptor, secretKey, null, signatureType); - } - - /** - * Add an inline-signature. - * Inline signatures are being embedded into the message itself and can be processed in one pass, thanks to the use - * of one-pass-signature packets. - * - * This method uses the passed in user-id to select user-specific hash algorithms. - * - * @param secretKeyDecryptor decryptor to unlock the signing secret key - * @param secretKey signing key - * @param userId user-id of the signer - * @param signatureType signature type (binary, canonical text) - * @return this - * @throws KeyValidationError if the key is invalid - * @throws PGPException if the key cannot be unlocked or the signing method cannot be created - */ - public SigningOptions addInlineSignature(SecretKeyRingProtector secretKeyDecryptor, - PGPSecretKeyRing secretKey, - String userId, - DocumentSignatureType signatureType) - throws KeyValidationError, PGPException { - KeyRingInfo keyRingInfo = new KeyRingInfo(secretKey, new Date()); - if (userId != null && !keyRingInfo.isUserIdValid(userId)) { - throw new KeyValidationError(userId, keyRingInfo.getLatestUserIdCertification(userId), keyRingInfo.getUserIdRevocation(userId)); - } - - List signingPubKeys = keyRingInfo.getSigningSubkeys(); - if (signingPubKeys.isEmpty()) { - throw new KeyCannotSignException("Key " + new OpenPgpV4Fingerprint(secretKey) + " has no valid signing key."); - } - - for (PGPPublicKey signingPubKey : signingPubKeys) { - PGPSecretKey signingSecKey = secretKey.getSecretKey(signingPubKey.getKeyID()); - PGPPrivateKey signingSubkey = UnlockSecretKey.unlockSecretKey(signingSecKey, secretKeyDecryptor); - Set hashAlgorithms = keyRingInfo.getPreferredHashAlgorithms(userId, signingPubKey.getKeyID()); - HashAlgorithm hashAlgorithm = negotiateHashAlgorithm(hashAlgorithms, PGPainless.getPolicy()); - addSigningMethod(secretKey, signingSubkey, hashAlgorithm, signatureType, false); - } - - return this; - } - - /** - * Add detached signatures with all key rings from the provided secret key ring collection. - * - * @param secretKeyDecryptor decryptor to unlock the secret signing keys - * @param signingKeys collection of signing key rings - * @param signatureType type of the signature (binary, canonical text) - * @return this - * @throws PGPException if any of the keys cannot be validated or unlocked, or if any signing method cannot be created - */ - public SigningOptions addDetachedSignatures(SecretKeyRingProtector secretKeyDecryptor, - PGPSecretKeyRingCollection signingKeys, - DocumentSignatureType signatureType) - throws PGPException { - for (PGPSecretKeyRing signingKey : signingKeys) { - addDetachedSignature(secretKeyDecryptor, signingKey, signatureType); - } - return this; - } - - /** - * Create a detached signature. - * Detached signatures are not being added into the PGP message itself. - * Instead they can be distributed separately to the message. - * Detached signatures are useful if the data that is being signed shall not be modified (eg. when signing a file). - * - * @param secretKeyDecryptor decryptor to unlock the secret signing key - * @param secretKey signing key - * @param signatureType type of data that is signed (binary, canonical text) - * @throws PGPException if the key cannot be validated or unlocked, or if no signature method can be created - * @return this - */ - public SigningOptions addDetachedSignature(SecretKeyRingProtector secretKeyDecryptor, - PGPSecretKeyRing secretKey, - DocumentSignatureType signatureType) - throws PGPException { - return addDetachedSignature(secretKeyDecryptor, secretKey, null, signatureType); - } - - /** - * Create a detached signature. - * Detached signatures are not being added into the PGP message itself. - * Instead they can be distributed separately to the message. - * Detached signatures are useful if the data that is being signed shall not be modified (eg. when signing a file). - * - * This method uses the passed in user-id to select user-specific hash algorithms. - * - * @param secretKeyDecryptor decryptor to unlock the secret signing key - * @param secretKey signing key - * @param userId user-id - * @param signatureType type of data that is signed (binary, canonical text) - * @throws PGPException if the key cannot be validated or unlocked, or if no signature method can be created - * @return this - */ - public SigningOptions addDetachedSignature(SecretKeyRingProtector secretKeyDecryptor, - PGPSecretKeyRing secretKey, - String userId, - DocumentSignatureType signatureType) - throws PGPException { - KeyRingInfo keyRingInfo = new KeyRingInfo(secretKey, new Date()); - if (userId != null && !keyRingInfo.isUserIdValid(userId)) { - throw new KeyValidationError(userId, keyRingInfo.getLatestUserIdCertification(userId), keyRingInfo.getUserIdRevocation(userId)); - } - - List signingPubKeys = keyRingInfo.getSigningSubkeys(); - if (signingPubKeys.isEmpty()) { - throw new KeyCannotSignException("Key has no valid signing key."); - } - - for (PGPPublicKey signingPubKey : signingPubKeys) { - PGPSecretKey signingSecKey = secretKey.getSecretKey(signingPubKey.getKeyID()); - PGPPrivateKey signingSubkey = signingSecKey.extractPrivateKey(secretKeyDecryptor.getDecryptor(signingPubKey.getKeyID())); - Set hashAlgorithms = keyRingInfo.getPreferredHashAlgorithms(userId, signingPubKey.getKeyID()); - HashAlgorithm hashAlgorithm = negotiateHashAlgorithm(hashAlgorithms, PGPainless.getPolicy()); - addSigningMethod(secretKey, signingSubkey, hashAlgorithm, signatureType, true); - } - - return this; - } - - private void addSigningMethod(PGPSecretKeyRing secretKey, - PGPPrivateKey signingSubkey, - HashAlgorithm hashAlgorithm, - DocumentSignatureType signatureType, - boolean detached) - throws PGPException { - SubkeyIdentifier signingKeyIdentifier = new SubkeyIdentifier(secretKey, signingSubkey.getKeyID()); - PGPSecretKey signingSecretKey = secretKey.getSecretKey(signingSubkey.getKeyID()); - PGPSignatureGenerator generator = createSignatureGenerator(signingSubkey, hashAlgorithm, signatureType); - generator.setUnhashedSubpackets(unhashedSubpackets(signingSecretKey).generate()); - SigningMethod signingMethod = detached ? - SigningMethod.detachedSignature(generator, hashAlgorithm) : - SigningMethod.inlineSignature(generator, hashAlgorithm); - signingMethods.put(signingKeyIdentifier, signingMethod); - } - - /** - * Negotiate, which hash algorithm to use. - * - * This method gives highest priority to the algorithm override, which can be set via {@link #overrideHashAlgorithm(HashAlgorithm)}. - * After that, the signing keys hash algorithm preferences are iterated to find the first acceptable algorithm. - * Lastly, should no acceptable algorithm be found, the {@link Policy Policies} default signature hash algorithm is - * used as a fallback. - * - * @param preferences preferences - * @param policy policy - * @return selected hash algorithm - */ - private HashAlgorithm negotiateHashAlgorithm(Set preferences, Policy policy) { - if (hashAlgorithmOverride != null) { - return hashAlgorithmOverride; - } - - HashAlgorithm algorithm = policy.getSignatureHashAlgorithmPolicy().defaultHashAlgorithm(); - if (preferences.isEmpty()) { - return algorithm; - } - - for (HashAlgorithm pref : preferences) { - if (policy.getSignatureHashAlgorithmPolicy().isAcceptable(pref)) { - return pref; - } - } - - return algorithm; - } - - private PGPSignatureGenerator createSignatureGenerator(PGPPrivateKey privateKey, - HashAlgorithm hashAlgorithm, - DocumentSignatureType signatureType) - throws PGPException { - int publicKeyAlgorithm = privateKey.getPublicKeyPacket().getAlgorithm(); - PGPContentSignerBuilder signerBuilder = ImplementationFactory.getInstance() - .getPGPContentSignerBuilder(publicKeyAlgorithm, hashAlgorithm.getAlgorithmId()); - PGPSignatureGenerator signatureGenerator = new PGPSignatureGenerator(signerBuilder); - signatureGenerator.init(signatureType.getSignatureType().getCode(), privateKey); - - return signatureGenerator; - } - - private PGPSignatureSubpacketGenerator unhashedSubpackets(PGPSecretKey key) { - PGPSignatureSubpacketGenerator generator = new PGPSignatureSubpacketGenerator(); - generator.setIssuerKeyID(false, key.getKeyID()); - generator.setIssuerFingerprint(false, key); - return generator; - } - - /** - * Return a map of key-ids and signing methods. - * For internal use. - * - * @return signing methods - */ - public Map getSigningMethods() { - return Collections.unmodifiableMap(signingMethods); - } - - /** - * Override hash algorithm negotiation by dictating which hash algorithm needs to be used. - * If no override has been set, an accetable algorithm will be negotiated instead. - * - * Note: To override the hash algorithm for signing, call this method *before* calling - * {@link #addInlineSignature(SecretKeyRingProtector, PGPSecretKeyRing, DocumentSignatureType)} or - * {@link #addDetachedSignature(SecretKeyRingProtector, PGPSecretKeyRing, DocumentSignatureType)}. - * - * @param hashAlgorithmOverride override hash algorithm - * @return this - */ - public SigningOptions overrideHashAlgorithm(HashAlgorithm hashAlgorithmOverride) { - this.hashAlgorithmOverride = hashAlgorithmOverride; - return this; - } - - /** - * Return the hash algorithm override (or null if no override is set). - * - * @return hash algorithm override - */ - public HashAlgorithm getHashAlgorithmOverride() { - return hashAlgorithmOverride; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/package-info.java deleted file mode 100644 index 4a2f1f39..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Classes used to encrypt or sign data using OpenPGP. - */ -package org.pgpainless.encryption_signing; diff --git a/pgpainless-core/src/main/java/org/pgpainless/exception/KeyCannotSignException.java b/pgpainless-core/src/main/java/org/pgpainless/exception/KeyCannotSignException.java deleted file mode 100644 index ee869fa9..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/exception/KeyCannotSignException.java +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.exception; - -import org.bouncycastle.openpgp.PGPException; - -public class KeyCannotSignException extends PGPException { - public KeyCannotSignException(String message) { - super(message); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/exception/KeyException.java b/pgpainless-core/src/main/java/org/pgpainless/exception/KeyException.java new file mode 100644 index 00000000..65d27390 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/exception/KeyException.java @@ -0,0 +1,132 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.exception; + +import org.bouncycastle.openpgp.PGPSignature; +import org.pgpainless.algorithm.PublicKeyAlgorithm; +import org.pgpainless.key.OpenPgpFingerprint; +import org.pgpainless.util.DateUtil; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Date; + +public abstract class KeyException extends RuntimeException { + + private final OpenPgpFingerprint fingerprint; + + protected KeyException(@Nonnull String message, @Nonnull OpenPgpFingerprint fingerprint) { + super(message); + this.fingerprint = fingerprint; + } + + protected KeyException(@Nonnull String message, @Nonnull OpenPgpFingerprint fingerprint, @Nonnull Throwable underlying) { + super(message, underlying); + this.fingerprint = fingerprint; + } + + public OpenPgpFingerprint getFingerprint() { + return fingerprint; + } + + public static class ExpiredKeyException extends KeyException { + + public ExpiredKeyException(@Nonnull OpenPgpFingerprint fingerprint, @Nonnull Date expirationDate) { + super("Key " + fingerprint + " is expired. Expiration date: " + DateUtil.formatUTCDate(expirationDate), fingerprint); + } + } + + public static class RevokedKeyException extends KeyException { + + public RevokedKeyException(@Nonnull OpenPgpFingerprint fingerprint) { + super("Key " + fingerprint + " appears to be revoked.", fingerprint); + } + } + + public static class UnacceptableEncryptionKeyException extends KeyException { + + public UnacceptableEncryptionKeyException(@Nonnull OpenPgpFingerprint fingerprint) { + super("Key " + fingerprint + " has no acceptable encryption key.", fingerprint); + } + + public UnacceptableEncryptionKeyException(@Nonnull PublicKeyAlgorithmPolicyException reason) { + super("Key " + reason.getFingerprint() + " has no acceptable encryption key.", reason.getFingerprint(), reason); + } + } + + public static class UnacceptableSigningKeyException extends KeyException { + + public UnacceptableSigningKeyException(@Nonnull OpenPgpFingerprint fingerprint) { + super("Key " + fingerprint + " has no acceptable signing key.", fingerprint); + } + + public UnacceptableSigningKeyException(@Nonnull PublicKeyAlgorithmPolicyException reason) { + super("Key " + reason.getFingerprint() + " has no acceptable signing key.", reason.getFingerprint(), reason); + } + } + + public static class UnacceptableThirdPartyCertificationKeyException extends KeyException { + + public UnacceptableThirdPartyCertificationKeyException(@Nonnull OpenPgpFingerprint fingerprint) { + super("Key " + fingerprint + " has no acceptable certification key.", fingerprint); + } + } + + public static class UnacceptableSelfSignatureException extends KeyException { + + public UnacceptableSelfSignatureException(@Nonnull OpenPgpFingerprint fingerprint) { + super("Key " + fingerprint + " does not have a valid/acceptable signature to derive an expiration date from.", fingerprint); + } + } + + public static class MissingSecretKeyException extends KeyException { + + private final long missingSecretKeyId; + + public MissingSecretKeyException(@Nonnull OpenPgpFingerprint fingerprint, long keyId) { + super("Key " + fingerprint + " does not contain a secret key for public key " + Long.toHexString(keyId), fingerprint); + this.missingSecretKeyId = keyId; + } + + public long getMissingSecretKeyId() { + return missingSecretKeyId; + } + } + + public static class PublicKeyAlgorithmPolicyException extends KeyException { + + private final long violatingSubkeyId; + + public PublicKeyAlgorithmPolicyException(@Nonnull OpenPgpFingerprint fingerprint, long keyId, @Nonnull PublicKeyAlgorithm algorithm, int bitSize) { + super("Subkey " + Long.toHexString(keyId) + " of key " + fingerprint + " is violating the Public Key Algorithm Policy:\n" + + algorithm + " of size " + bitSize + " is not acceptable.", fingerprint); + this.violatingSubkeyId = keyId; + } + + public long getViolatingSubkeyId() { + return violatingSubkeyId; + } + } + + public static class UnboundUserIdException extends KeyException { + + public UnboundUserIdException(@Nonnull OpenPgpFingerprint fingerprint, @Nonnull String userId, + @Nullable PGPSignature userIdSignature, @Nullable PGPSignature userIdRevocation) { + super(errorMessage(fingerprint, userId, userIdSignature, userIdRevocation), fingerprint); + } + + private static String errorMessage(@Nonnull OpenPgpFingerprint fingerprint, @Nonnull String userId, + @Nullable PGPSignature userIdSignature, @Nullable PGPSignature userIdRevocation) { + String errorMessage = "UserID '" + userId + "' is not valid for key " + fingerprint + ": "; + if (userIdSignature == null) { + return errorMessage + "Missing binding signature."; + } + if (userIdRevocation != null) { + return errorMessage + "UserID is revoked."; + } + return errorMessage + "Unacceptable binding signature."; + } + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/exception/KeyIntegrityException.java b/pgpainless-core/src/main/java/org/pgpainless/exception/KeyIntegrityException.java new file mode 100644 index 00000000..b7a87ab7 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/exception/KeyIntegrityException.java @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: 2021 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.exception; + +/** + * This exception gets thrown, when the integrity of an OpenPGP key is broken. + * That could happen on accident, or during an active attack, so take this exception seriously. + */ +public class KeyIntegrityException extends AssertionError { + + public KeyIntegrityException() { + super("Key Integrity Exception"); + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/exception/KeyValidationError.java b/pgpainless-core/src/main/java/org/pgpainless/exception/KeyValidationError.java deleted file mode 100644 index 8296d6c9..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/exception/KeyValidationError.java +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.exception; - -import org.bouncycastle.openpgp.PGPSignature; - -public class KeyValidationError extends AssertionError { - - public KeyValidationError(String userId, PGPSignature userIdSig, PGPSignature userIdRevocation) { - super("User-ID '" + userId + "' is not valid: Sig: " + userIdSig + " Rev: " + userIdRevocation); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java b/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java new file mode 100644 index 00000000..f98a4048 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.exception; + +import org.pgpainless.decryption_verification.syntax_check.InputSymbol; +import org.pgpainless.decryption_verification.syntax_check.StackSymbol; +import org.pgpainless.decryption_verification.syntax_check.State; + +/** + * Exception that gets thrown if the OpenPGP message is malformed. + * Malformed messages are messages which do not follow the grammar specified in the RFC. + * + * @see RFC4880 §11.3. OpenPGP Messages + */ +public class MalformedOpenPgpMessageException extends RuntimeException { + + public MalformedOpenPgpMessageException(String message) { + super(message); + } + + public MalformedOpenPgpMessageException(State state, InputSymbol input, StackSymbol stackItem) { + this("There is no legal transition from state '" + state + "' for input '" + input + "' when '" + stackItem + "' is on top of the stack."); + } + + public MalformedOpenPgpMessageException(String s, MalformedOpenPgpMessageException e) { + super(s, e); + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/exception/MissingLiteralDataException.java b/pgpainless-core/src/main/java/org/pgpainless/exception/MissingLiteralDataException.java deleted file mode 100644 index e396b1df..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/exception/MissingLiteralDataException.java +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.exception; - -import org.bouncycastle.openpgp.PGPException; - -/** - * Exception that gets thrown if a {@link org.bouncycastle.bcpg.LiteralDataPacket} is expected, but not found. - */ -public class MissingLiteralDataException extends PGPException { - - public MissingLiteralDataException(String message) { - super(message); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/exception/NotYetImplementedException.java b/pgpainless-core/src/main/java/org/pgpainless/exception/NotYetImplementedException.java deleted file mode 100644 index 72f9b569..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/exception/NotYetImplementedException.java +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.exception; - -/** - * Method that gets thrown if the user requests some functionality which is not yet implemented. - */ -public class NotYetImplementedException extends RuntimeException { - -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/exception/SignatureValidationException.java b/pgpainless-core/src/main/java/org/pgpainless/exception/SignatureValidationException.java index b5b8941d..2141ec5c 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/exception/SignatureValidationException.java +++ b/pgpainless-core/src/main/java/org/pgpainless/exception/SignatureValidationException.java @@ -28,7 +28,14 @@ public class SignatureValidationException extends PGPException { StringBuilder sb = new StringBuilder(); sb.append(rejections.size()).append(" rejected signatures:\n"); for (PGPSignature signature : rejections.keySet()) { - sb.append(SignatureType.valueOf(signature.getSignatureType())).append(' ') + String typeString; + SignatureType type = SignatureType.fromCode(signature.getSignatureType()); + if (type == null) { + typeString = "0x" + Long.toHexString(signature.getSignatureType()); + } else { + typeString = type.toString(); + } + sb.append(typeString).append(' ') .append(signature.getCreationTime()).append(": ") .append(rejections.get(signature).getMessage()).append('\n'); } diff --git a/pgpainless-core/src/main/java/org/pgpainless/implementation/BcImplementationFactory.java b/pgpainless-core/src/main/java/org/pgpainless/implementation/BcImplementationFactory.java deleted file mode 100644 index ea9b0eb0..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/implementation/BcImplementationFactory.java +++ /dev/null @@ -1,151 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.implementation; - -import java.security.KeyPair; -import java.util.Date; - -import org.bouncycastle.crypto.AsymmetricCipherKeyPair; -import org.bouncycastle.crypto.params.AsymmetricKeyParameter; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPKeyPair; -import org.bouncycastle.openpgp.PGPPrivateKey; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.operator.KeyFingerPrintCalculator; -import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; -import org.bouncycastle.openpgp.operator.PBEKeyEncryptionMethodGenerator; -import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; -import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; -import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder; -import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; -import org.bouncycastle.openpgp.operator.PGPDataEncryptorBuilder; -import org.bouncycastle.openpgp.operator.PGPDigestCalculator; -import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; -import org.bouncycastle.openpgp.operator.PublicKeyKeyEncryptionMethodGenerator; -import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator; -import org.bouncycastle.openpgp.operator.bc.BcPBEDataDecryptorFactory; -import org.bouncycastle.openpgp.operator.bc.BcPBEKeyEncryptionMethodGenerator; -import org.bouncycastle.openpgp.operator.bc.BcPBESecretKeyDecryptorBuilder; -import org.bouncycastle.openpgp.operator.bc.BcPBESecretKeyEncryptorBuilder; -import org.bouncycastle.openpgp.operator.bc.BcPGPContentSignerBuilder; -import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider; -import org.bouncycastle.openpgp.operator.bc.BcPGPDataEncryptorBuilder; -import org.bouncycastle.openpgp.operator.bc.BcPGPDigestCalculatorProvider; -import org.bouncycastle.openpgp.operator.bc.BcPGPKeyConverter; -import org.bouncycastle.openpgp.operator.bc.BcPGPKeyPair; -import org.bouncycastle.openpgp.operator.bc.BcPublicKeyDataDecryptorFactory; -import org.bouncycastle.openpgp.operator.bc.BcPublicKeyKeyEncryptionMethodGenerator; -import org.bouncycastle.openpgp.operator.jcajce.JcaPGPKeyPair; -import org.pgpainless.algorithm.HashAlgorithm; -import org.pgpainless.algorithm.PublicKeyAlgorithm; -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; -import org.pgpainless.util.Passphrase; - -public class BcImplementationFactory extends ImplementationFactory { - - @Override - public PBESecretKeyEncryptor getPBESecretKeyEncryptor(PGPSecretKey secretKey, Passphrase passphrase) - throws PGPException { - int keyEncryptionAlgorithm = secretKey.getKeyEncryptionAlgorithm(); - - if (secretKey.getS2K() == null) { - return getPBESecretKeyEncryptor(SymmetricKeyAlgorithm.fromId(keyEncryptionAlgorithm), passphrase); - } - - int hashAlgorithm = secretKey.getS2K().getHashAlgorithm(); - PGPDigestCalculator digestCalculator = getPGPDigestCalculator(hashAlgorithm); - long iterationCount = secretKey.getS2K().getIterationCount(); - - return new BcPBESecretKeyEncryptorBuilder(keyEncryptionAlgorithm, digestCalculator, (int) iterationCount) - .build(passphrase.getChars()); - } - - @Override - public PBESecretKeyEncryptor getPBESecretKeyEncryptor(SymmetricKeyAlgorithm symmetricKeyAlgorithm, - PGPDigestCalculator digestCalculator, - Passphrase passphrase) { - return new BcPBESecretKeyEncryptorBuilder(symmetricKeyAlgorithm.getAlgorithmId(), digestCalculator) - .build(passphrase.getChars()); - } - - @Override - public PBESecretKeyDecryptor getPBESecretKeyDecryptor(Passphrase passphrase) { - return new BcPBESecretKeyDecryptorBuilder(getPGPDigestCalculatorProvider()) - .build(passphrase.getChars()); - } - - @Override - public BcPGPDigestCalculatorProvider getPGPDigestCalculatorProvider() { - return new BcPGPDigestCalculatorProvider(); - } - - @Override - public PGPContentVerifierBuilderProvider getPGPContentVerifierBuilderProvider() { - return new BcPGPContentVerifierBuilderProvider(); - } - - @Override - public PGPContentSignerBuilder getPGPContentSignerBuilder(int keyAlgorithm, int hashAlgorithm) { - return new BcPGPContentSignerBuilder(keyAlgorithm, hashAlgorithm); - } - - @Override - public KeyFingerPrintCalculator getKeyFingerprintCalculator() { - return new BcKeyFingerprintCalculator(); - } - - @Override - public PBEDataDecryptorFactory getPBEDataDecryptorFactory(Passphrase passphrase) { - return new BcPBEDataDecryptorFactory(passphrase.getChars(), getPGPDigestCalculatorProvider()); - } - - @Override - public PublicKeyDataDecryptorFactory getPublicKeyDataDecryptorFactory(PGPPrivateKey privateKey) { - return new BcPublicKeyDataDecryptorFactory(privateKey); - } - - @Override - public PublicKeyKeyEncryptionMethodGenerator getPublicKeyKeyEncryptionMethodGenerator(PGPPublicKey key) { - return new BcPublicKeyKeyEncryptionMethodGenerator(key); - } - - @Override - public PBEKeyEncryptionMethodGenerator getPBEKeyEncryptionMethodGenerator(Passphrase passphrase) { - return new BcPBEKeyEncryptionMethodGenerator(passphrase.getChars()); - } - - @Override - public PGPDataEncryptorBuilder getPGPDataEncryptorBuilder(int symmetricKeyAlgorithm) { - return new BcPGPDataEncryptorBuilder(symmetricKeyAlgorithm); - } - - @Override - public PGPKeyPair getPGPKeyPair(PublicKeyAlgorithm algorithm, KeyPair keyPair, Date creationDate) - throws PGPException { - return new BcPGPKeyPair(algorithm.getAlgorithmId(), jceToBcKeyPair(algorithm, keyPair, creationDate), creationDate); - } - - @Override - public PBESecretKeyEncryptor getPBESecretKeyEncryptor(SymmetricKeyAlgorithm encryptionAlgorithm, HashAlgorithm hashAlgorithm, int s2kCount, Passphrase passphrase) throws PGPException { - return new BcPBESecretKeyEncryptorBuilder( - encryptionAlgorithm.getAlgorithmId(), - getPGPDigestCalculator(hashAlgorithm), - s2kCount) - .build(passphrase.getChars()); - } - - private AsymmetricCipherKeyPair jceToBcKeyPair(PublicKeyAlgorithm algorithm, - KeyPair keyPair, - Date creationDate) throws PGPException { - BcPGPKeyConverter converter = new BcPGPKeyConverter(); - - PGPKeyPair pair = new JcaPGPKeyPair(algorithm.getAlgorithmId(), keyPair, creationDate); - AsymmetricKeyParameter publicKey = converter.getPublicKey(pair.getPublicKey()); - AsymmetricKeyParameter privateKey = converter.getPrivateKey(pair.getPrivateKey()); - - return new AsymmetricCipherKeyPair(publicKey, privateKey); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/implementation/ImplementationFactory.java b/pgpainless-core/src/main/java/org/pgpainless/implementation/ImplementationFactory.java deleted file mode 100644 index 6f5ea044..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/implementation/ImplementationFactory.java +++ /dev/null @@ -1,106 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.implementation; - -import java.security.KeyPair; -import java.util.Date; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPKeyPair; -import org.bouncycastle.openpgp.PGPPrivateKey; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.operator.KeyFingerPrintCalculator; -import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; -import org.bouncycastle.openpgp.operator.PBEKeyEncryptionMethodGenerator; -import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; -import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; -import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder; -import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; -import org.bouncycastle.openpgp.operator.PGPDataEncryptorBuilder; -import org.bouncycastle.openpgp.operator.PGPDigestCalculator; -import org.bouncycastle.openpgp.operator.PGPDigestCalculatorProvider; -import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; -import org.bouncycastle.openpgp.operator.PublicKeyKeyEncryptionMethodGenerator; -import org.pgpainless.algorithm.HashAlgorithm; -import org.pgpainless.algorithm.PublicKeyAlgorithm; -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; -import org.pgpainless.util.Passphrase; - -public abstract class ImplementationFactory { - - private static ImplementationFactory FACTORY_IMPLEMENTATION; - - public static void setFactoryImplementation(ImplementationFactory implementation) { - FACTORY_IMPLEMENTATION = implementation; - } - - public static ImplementationFactory getInstance() { - if (FACTORY_IMPLEMENTATION == null) { - FACTORY_IMPLEMENTATION = new BcImplementationFactory(); - } - return FACTORY_IMPLEMENTATION; - } - - public PBESecretKeyEncryptor getPBESecretKeyEncryptor(SymmetricKeyAlgorithm symmetricKeyAlgorithm, - Passphrase passphrase) - throws PGPException { - return getPBESecretKeyEncryptor(symmetricKeyAlgorithm, - getPGPDigestCalculator(HashAlgorithm.SHA1), passphrase); - } - - public abstract PBESecretKeyEncryptor getPBESecretKeyEncryptor(PGPSecretKey secretKey, Passphrase passphrase) throws PGPException; - - public abstract PBESecretKeyEncryptor getPBESecretKeyEncryptor(SymmetricKeyAlgorithm symmetricKeyAlgorithm, - PGPDigestCalculator digestCalculator, - Passphrase passphrase); - - public abstract PBESecretKeyDecryptor getPBESecretKeyDecryptor(Passphrase passphrase) throws PGPException; - - public PGPDigestCalculator getPGPDigestCalculator(HashAlgorithm algorithm) throws PGPException { - return getPGPDigestCalculator(algorithm.getAlgorithmId()); - } - - public PGPDigestCalculator getPGPDigestCalculator(int algorithm) throws PGPException { - return getPGPDigestCalculatorProvider().get(algorithm); - } - - public abstract PGPDigestCalculatorProvider getPGPDigestCalculatorProvider() throws PGPException; - - public abstract PGPContentVerifierBuilderProvider getPGPContentVerifierBuilderProvider(); - - public PGPContentSignerBuilder getPGPContentSignerBuilder(PublicKeyAlgorithm keyAlgorithm, HashAlgorithm hashAlgorithm) { - return getPGPContentSignerBuilder(keyAlgorithm.getAlgorithmId(), hashAlgorithm.getAlgorithmId()); - } - - public abstract PGPContentSignerBuilder getPGPContentSignerBuilder(int keyAlgorithm, int hashAlgorithm); - - public abstract KeyFingerPrintCalculator getKeyFingerprintCalculator(); - - public abstract PBEDataDecryptorFactory getPBEDataDecryptorFactory(Passphrase passphrase) throws PGPException; - - public abstract PublicKeyDataDecryptorFactory getPublicKeyDataDecryptorFactory(PGPPrivateKey privateKey); - - public abstract PublicKeyKeyEncryptionMethodGenerator getPublicKeyKeyEncryptionMethodGenerator(PGPPublicKey key); - - public abstract PBEKeyEncryptionMethodGenerator getPBEKeyEncryptionMethodGenerator(Passphrase passphrase); - - public PGPDataEncryptorBuilder getPGPDataEncryptorBuilder(SymmetricKeyAlgorithm symmetricKeyAlgorithm) { - return getPGPDataEncryptorBuilder(symmetricKeyAlgorithm.getAlgorithmId()); - } - - public abstract PGPDataEncryptorBuilder getPGPDataEncryptorBuilder(int symmetricKeyAlgorithm); - - public abstract PGPKeyPair getPGPKeyPair(PublicKeyAlgorithm algorithm, KeyPair keyPair, Date creationDate) throws PGPException; - - public abstract PBESecretKeyEncryptor getPBESecretKeyEncryptor(SymmetricKeyAlgorithm encryptionAlgorithm, - HashAlgorithm hashAlgorithm, int s2kCount, - Passphrase passphrase) throws PGPException; - - @Override - public String toString() { - return getClass().getSimpleName(); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/implementation/JceImplementationFactory.java b/pgpainless-core/src/main/java/org/pgpainless/implementation/JceImplementationFactory.java deleted file mode 100644 index cbacdf1b..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/implementation/JceImplementationFactory.java +++ /dev/null @@ -1,127 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.implementation; - -import java.security.KeyPair; -import java.util.Date; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPKeyPair; -import org.bouncycastle.openpgp.PGPPrivateKey; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.operator.KeyFingerPrintCalculator; -import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; -import org.bouncycastle.openpgp.operator.PBEKeyEncryptionMethodGenerator; -import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; -import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; -import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder; -import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; -import org.bouncycastle.openpgp.operator.PGPDataEncryptorBuilder; -import org.bouncycastle.openpgp.operator.PGPDigestCalculator; -import org.bouncycastle.openpgp.operator.PGPDigestCalculatorProvider; -import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; -import org.bouncycastle.openpgp.operator.PublicKeyKeyEncryptionMethodGenerator; -import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator; -import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder; -import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider; -import org.bouncycastle.openpgp.operator.jcajce.JcaPGPDigestCalculatorProviderBuilder; -import org.bouncycastle.openpgp.operator.jcajce.JcaPGPKeyPair; -import org.bouncycastle.openpgp.operator.jcajce.JcePBEDataDecryptorFactoryBuilder; -import org.bouncycastle.openpgp.operator.jcajce.JcePBEKeyEncryptionMethodGenerator; -import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder; -import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyEncryptorBuilder; -import org.bouncycastle.openpgp.operator.jcajce.JcePGPDataEncryptorBuilder; -import org.bouncycastle.openpgp.operator.jcajce.JcePublicKeyDataDecryptorFactoryBuilder; -import org.bouncycastle.openpgp.operator.jcajce.JcePublicKeyKeyEncryptionMethodGenerator; -import org.pgpainless.algorithm.HashAlgorithm; -import org.pgpainless.algorithm.PublicKeyAlgorithm; -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; -import org.pgpainless.provider.ProviderFactory; -import org.pgpainless.util.Passphrase; - -public class JceImplementationFactory extends ImplementationFactory { - - public PBESecretKeyEncryptor getPBESecretKeyEncryptor(PGPSecretKey secretKey, Passphrase passphrase) { - return new JcePBESecretKeyEncryptorBuilder(secretKey.getKeyEncryptionAlgorithm()) - .setProvider(ProviderFactory.getProvider()) - .build(passphrase.getChars()); - } - - public PBESecretKeyEncryptor getPBESecretKeyEncryptor(SymmetricKeyAlgorithm symmetricKeyAlgorithm, PGPDigestCalculator digestCalculator, Passphrase passphrase) { - return new JcePBESecretKeyEncryptorBuilder(symmetricKeyAlgorithm.getAlgorithmId(), digestCalculator) - .setProvider(ProviderFactory.getProvider()) - .build(passphrase.getChars()); - } - - public PBESecretKeyDecryptor getPBESecretKeyDecryptor(Passphrase passphrase) throws PGPException { - return new JcePBESecretKeyDecryptorBuilder(getPGPDigestCalculatorProvider()) - .setProvider(ProviderFactory.getProvider()) - .build(passphrase.getChars()); - } - - public PGPDigestCalculatorProvider getPGPDigestCalculatorProvider() - throws PGPException { - return new JcaPGPDigestCalculatorProviderBuilder() - .setProvider(ProviderFactory.getProvider()) - .build(); - } - - public PGPContentVerifierBuilderProvider getPGPContentVerifierBuilderProvider() { - return new JcaPGPContentVerifierBuilderProvider() - .setProvider(ProviderFactory.getProvider()); - } - - public PGPContentSignerBuilder getPGPContentSignerBuilder(int keyAlgorithm, int hashAlgorithm) { - return new JcaPGPContentSignerBuilder(keyAlgorithm, hashAlgorithm) - .setProvider(ProviderFactory.getProvider()); - } - - public KeyFingerPrintCalculator getKeyFingerprintCalculator() { - return new JcaKeyFingerprintCalculator() - .setProvider(ProviderFactory.getProvider()); - } - - public PBEDataDecryptorFactory getPBEDataDecryptorFactory(Passphrase passphrase) - throws PGPException { - return new JcePBEDataDecryptorFactoryBuilder(getPGPDigestCalculatorProvider()) - .setProvider(ProviderFactory.getProvider()) - .build(passphrase.getChars()); - } - - public PublicKeyDataDecryptorFactory getPublicKeyDataDecryptorFactory(PGPPrivateKey privateKey) { - return new JcePublicKeyDataDecryptorFactoryBuilder() - .setProvider(ProviderFactory.getProvider()) - .build(privateKey); - } - - public PublicKeyKeyEncryptionMethodGenerator getPublicKeyKeyEncryptionMethodGenerator(PGPPublicKey key) { - return new JcePublicKeyKeyEncryptionMethodGenerator(key) - .setProvider(ProviderFactory.getProvider()); - } - - public PBEKeyEncryptionMethodGenerator getPBEKeyEncryptionMethodGenerator(Passphrase passphrase) { - return new JcePBEKeyEncryptionMethodGenerator(passphrase.getChars()) - .setProvider(ProviderFactory.getProvider()); - } - - public PGPDataEncryptorBuilder getPGPDataEncryptorBuilder(int symmetricKeyAlgorithm) { - return new JcePGPDataEncryptorBuilder(symmetricKeyAlgorithm) - .setProvider(ProviderFactory.getProvider()); - } - - public PGPKeyPair getPGPKeyPair(PublicKeyAlgorithm algorithm, KeyPair keyPair, Date creationDate) throws PGPException { - return new JcaPGPKeyPair(algorithm.getAlgorithmId(), keyPair, creationDate); - } - - public PBESecretKeyEncryptor getPBESecretKeyEncryptor(SymmetricKeyAlgorithm encryptionAlgorithm, HashAlgorithm hashAlgorithm, int s2kCount, Passphrase passphrase) throws PGPException { - return new JcePBESecretKeyEncryptorBuilder( - encryptionAlgorithm.getAlgorithmId(), - getPGPDigestCalculator(hashAlgorithm), - s2kCount) - .setProvider(ProviderFactory.getProvider()) - .build(passphrase.getChars()); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/implementation/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/implementation/package-info.java deleted file mode 100644 index 3ce87531..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/implementation/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Implementation factory classes to be able to switch out the underlying crypto engine implementation. - */ -package org.pgpainless.implementation; diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/KeyRingValidator.java b/pgpainless-core/src/main/java/org/pgpainless/key/KeyRingValidator.java deleted file mode 100644 index 5809ad1a..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/KeyRingValidator.java +++ /dev/null @@ -1,138 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key; - -import java.util.Collections; -import java.util.Date; -import java.util.Iterator; -import java.util.List; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPKeyRing; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.PGPUserAttributeSubpacketVector; -import org.pgpainless.algorithm.SignatureType; -import org.pgpainless.exception.SignatureValidationException; -import org.pgpainless.implementation.ImplementationFactory; -import org.pgpainless.policy.Policy; -import org.pgpainless.signature.SignatureCreationDateComparator; -import org.pgpainless.signature.SignatureVerifier; -import org.pgpainless.util.CollectionUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public final class KeyRingValidator { - - private KeyRingValidator() { - - } - - private static final Logger LOGGER = LoggerFactory.getLogger(KeyRingValidator.class); - - public static R validate(R keyRing, Policy policy) { - try { - return validate(keyRing, policy, new Date()); - } catch (PGPException e) { - return null; - } - } - - public static R validate(R keyRing, Policy policy, Date validationDate) throws PGPException { - return getKeyRingAtDate(keyRing, policy, validationDate); - } - - private static R getKeyRingAtDate(R keyRing, Policy policy, Date validationDate) throws PGPException { - PGPPublicKey primaryKey = keyRing.getPublicKey(); - primaryKey = evaluatePrimaryKey(primaryKey, policy, validationDate); - if (keyRing instanceof PGPPublicKeyRing) { - PGPPublicKeyRing publicKeys = (PGPPublicKeyRing) keyRing; - publicKeys = PGPPublicKeyRing.insertPublicKey(publicKeys, primaryKey); - keyRing = (R) publicKeys; - } - - return keyRing; - } - - private static PGPPublicKey evaluatePrimaryKey(PGPPublicKey primaryKey, Policy policy, Date validationDate) throws PGPException { - - PGPPublicKey blank = new PGPPublicKey(primaryKey.getPublicKeyPacket(), ImplementationFactory.getInstance().getKeyFingerprintCalculator()); - - Iterator directKeyIterator = primaryKey.getSignaturesOfType(SignatureType.DIRECT_KEY.getCode()); - List directKeyCertifications = CollectionUtils.iteratorToList(directKeyIterator); - Collections.sort(directKeyCertifications, new SignatureCreationDateComparator(SignatureCreationDateComparator.Order.NEW_TO_OLD)); - for (PGPSignature signature : directKeyCertifications) { - try { - if (SignatureVerifier.verifyDirectKeySignature(signature, blank, policy, validationDate)) { - blank = PGPPublicKey.addCertification(blank, signature); - } - } catch (SignatureValidationException e) { - LOGGER.debug("Rejecting direct key signature: {}", e.getMessage(), e); - } - } - - Iterator revocationIterator = primaryKey.getSignaturesOfType(SignatureType.KEY_REVOCATION.getCode()); - List directKeyRevocations = CollectionUtils.iteratorToList(revocationIterator); - Collections.sort(directKeyRevocations, new SignatureCreationDateComparator(SignatureCreationDateComparator.Order.NEW_TO_OLD)); - for (PGPSignature signature : directKeyRevocations) { - try { - if (SignatureVerifier.verifyKeyRevocationSignature(signature, primaryKey, policy, validationDate)) { - blank = PGPPublicKey.addCertification(blank, signature); - } - } catch (SignatureValidationException e) { - LOGGER.debug("Rejecting key revocation signature: {}", e.getMessage(), e); - } - } - - Iterator userIdIterator = primaryKey.getUserIDs(); - while (userIdIterator.hasNext()) { - String userId = userIdIterator.next(); - Iterator userIdSigs = primaryKey.getSignaturesForID(userId); - List signatures = CollectionUtils.iteratorToList(userIdSigs); - Collections.sort(signatures, new SignatureCreationDateComparator(SignatureCreationDateComparator.Order.NEW_TO_OLD)); - for (PGPSignature signature : signatures) { - try { - if (SignatureType.valueOf(signature.getSignatureType()) == SignatureType.CERTIFICATION_REVOCATION) { - if (SignatureVerifier.verifyUserIdRevocation(userId, signature, primaryKey, policy, validationDate)) { - blank = PGPPublicKey.addCertification(blank, userId, signature); - } - } else { - if (SignatureVerifier.verifyUserIdCertification(userId, signature, primaryKey, policy, validationDate)) { - blank = PGPPublicKey.addCertification(blank, userId, signature); - } - } - } catch (SignatureValidationException e) { - LOGGER.debug("Rejecting user-id certification for user-id {}: {}", userId, e.getMessage(), e); - } - } - } - - Iterator userAttributes = primaryKey.getUserAttributes(); - while (userAttributes.hasNext()) { - PGPUserAttributeSubpacketVector userAttribute = userAttributes.next(); - Iterator userAttributeSignatureIterator = primaryKey.getSignaturesForUserAttribute(userAttribute); - while (userAttributeSignatureIterator.hasNext()) { - PGPSignature signature = userAttributeSignatureIterator.next(); - try { - if (SignatureType.valueOf(signature.getSignatureType()) == SignatureType.CERTIFICATION_REVOCATION) { - if (SignatureVerifier.verifyUserAttributesRevocation(userAttribute, signature, primaryKey, policy, validationDate)) { - blank = PGPPublicKey.addCertification(blank, userAttribute, signature); - } - } else { - if (SignatureVerifier.verifyUserAttributesCertification(userAttribute, signature, primaryKey, policy, validationDate)) { - blank = PGPPublicKey.addCertification(blank, userAttribute, signature); - } - } - } catch (SignatureValidationException e) { - LOGGER.debug("Rejecting user-attribute signature: {}", e.getMessage(), e); - } - } - } - - return blank; - } - -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpV4Fingerprint.java b/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpV4Fingerprint.java deleted file mode 100644 index b368e370..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpV4Fingerprint.java +++ /dev/null @@ -1,186 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key; - -import java.net.URI; -import java.net.URISyntaxException; -import java.nio.Buffer; -import java.nio.ByteBuffer; -import java.nio.charset.Charset; -import javax.annotation.Nonnull; - -import org.bouncycastle.openpgp.PGPKeyRing; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.util.encoders.Hex; - -/** - * This class represents an hex encoded, uppercase OpenPGP v4 fingerprint. - */ -public class OpenPgpV4Fingerprint implements CharSequence, Comparable { - - public static final String SCHEME = "openpgp4fpr"; - - private static final Charset utf8 = Charset.forName("UTF-8"); - private final String fingerprint; - - /** - * Create an {@link OpenPgpV4Fingerprint}. - * @see - * XEP-0373 §4.1: The OpenPGP Public-Key Data Node about how to obtain the fingerprint - * @param fingerprint hexadecimal representation of the fingerprint. - */ - public OpenPgpV4Fingerprint(@Nonnull String fingerprint) { - String fp = fingerprint.replace(" ", "").trim().toUpperCase(); - if (!isValid(fp)) { - throw new IllegalArgumentException("Fingerprint " + fingerprint + - " does not appear to be a valid OpenPGP v4 fingerprint."); - } - this.fingerprint = fp; - } - - public OpenPgpV4Fingerprint(@Nonnull byte[] bytes) { - this(new String(bytes, utf8)); - } - - public OpenPgpV4Fingerprint(@Nonnull PGPPublicKey key) { - this(Hex.encode(key.getFingerprint())); - if (key.getVersion() != 4) { - throw new IllegalArgumentException("Key is not a v4 OpenPgp key."); - } - } - - public OpenPgpV4Fingerprint(@Nonnull PGPSecretKey key) { - this(key.getPublicKey()); - } - - public OpenPgpV4Fingerprint(@Nonnull PGPPublicKeyRing ring) { - this(ring.getPublicKey()); - } - - public OpenPgpV4Fingerprint(@Nonnull PGPSecretKeyRing ring) { - this(ring.getPublicKey()); - } - - public OpenPgpV4Fingerprint(@Nonnull PGPKeyRing ring) { - this(ring.getPublicKey()); - } - - /** - * Check, whether the fingerprint consists of 40 valid hexadecimal characters. - * @param fp fingerprint to check. - * @return true if fingerprint is valid. - */ - private static boolean isValid(@Nonnull String fp) { - return fp.matches("[0-9A-F]{40}"); - } - - /** - * Return the key id of the OpenPGP public key this {@link OpenPgpV4Fingerprint} belongs to. - * - * @see - * RFC-4880 §12.2: Key IDs and Fingerprints - * @return key id - */ - public long getKeyId() { - byte[] bytes = Hex.decode(toString().getBytes(utf8)); - ByteBuffer buf = ByteBuffer.wrap(bytes); - - // We have to cast here in order to be compatible with java 8 - // https://github.com/eclipse/jetty.project/issues/3244 - ((Buffer) buf).position(12); - - return buf.getLong(); - } - - @Override - public boolean equals(Object other) { - if (other == null) { - return false; - } - - if (!(other instanceof CharSequence)) { - return false; - } - - return this.toString().equals(other.toString()); - } - - @Override - public int hashCode() { - return fingerprint.hashCode(); - } - - @Override - public int length() { - return fingerprint.length(); - } - - @Override - public char charAt(int i) { - return fingerprint.charAt(i); - } - - @Override - public CharSequence subSequence(int i, int i1) { - return fingerprint.subSequence(i, i1); - } - - @Override - @Nonnull - public String toString() { - return fingerprint; - } - - public String prettyPrint() { - String fp = toString(); - StringBuilder pretty = new StringBuilder(); - for (int i = 0; i < 5; i++) { - pretty.append(fp, i * 4, (i + 1) * 4).append(' '); - } - pretty.append(' '); - for (int i = 5; i < 9; i++) { - pretty.append(fp, i * 4, (i + 1) * 4).append(' '); - } - pretty.append(fp, 36, 40); - return pretty.toString(); - } - - /** - * Return the fingerprint as an openpgp4fpr {@link URI}. - * An example would be 'openpgp4fpr:7F9116FEA90A5983936C7CFAA027DB2F3E1E118A'. - * - * @return openpgp4fpr fingerprint uri - * @see openpgp4fpr URI scheme - */ - public URI toUri() { - try { - return new URI(SCHEME, toString(), null); - } catch (URISyntaxException e) { - throw new AssertionError(e); - } - } - - /** - * Convert a openpgp4fpr URI to an {@link OpenPgpV4Fingerprint}. - * - * @param uri {@link URI} with scheme 'openpgp4fpr' - * @return fingerprint parsed from the uri - * @see openpgp4fpr URI scheme - */ - public static OpenPgpV4Fingerprint fromUri(URI uri) { - if (!SCHEME.equals(uri.getScheme())) { - throw new IllegalArgumentException("URI scheme MUST equal '" + SCHEME + "'"); - } - return new OpenPgpV4Fingerprint(uri.getSchemeSpecificPart()); - } - - @Override - public int compareTo(@Nonnull OpenPgpV4Fingerprint openPgpV4Fingerprint) { - return fingerprint.compareTo(openPgpV4Fingerprint.fingerprint); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/SubkeyIdentifier.java b/pgpainless-core/src/main/java/org/pgpainless/key/SubkeyIdentifier.java deleted file mode 100644 index afd093af..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/SubkeyIdentifier.java +++ /dev/null @@ -1,146 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key; - -import java.util.NoSuchElementException; -import javax.annotation.Nonnull; - -import org.bouncycastle.openpgp.PGPKeyRing; -import org.bouncycastle.openpgp.PGPPublicKey; - -/** - * Tuple class used to identify a subkey by fingerprints of the primary key of the subkeys key ring, - * as well as the subkeys fingerprint. - */ -public class SubkeyIdentifier { - - private final OpenPgpV4Fingerprint primaryKeyFingerprint; - private final OpenPgpV4Fingerprint subkeyFingerprint; - - /** - * Create a {@link SubkeyIdentifier} from a {@link PGPKeyRing}. - * The identifier will point to the primary key of the provided ring. - * - * @param keyRing key ring - */ - public SubkeyIdentifier(PGPKeyRing keyRing) { - this(keyRing, keyRing.getPublicKey().getKeyID()); - } - - /** - * Create a {@link SubkeyIdentifier} from a {@link PGPKeyRing} and the subkeys key id. - * {@link #getPrimaryKeyFingerprint()} will return the {@link OpenPgpV4Fingerprint} of the keyrings primary key, - * while {@link #getSubkeyFingerprint()} will return the subkeys fingerprint. - * - * @param keyRing keyring the subkey belongs to - * @param keyId keyid of the subkey - */ - public SubkeyIdentifier(@Nonnull PGPKeyRing keyRing, long keyId) { - PGPPublicKey subkey = keyRing.getPublicKey(keyId); - if (subkey == null) { - throw new NoSuchElementException("Key ring does not contain subkey with id " + Long.toHexString(keyId)); - } - this.primaryKeyFingerprint = new OpenPgpV4Fingerprint(keyRing); - this.subkeyFingerprint = new OpenPgpV4Fingerprint(subkey); - } - - public SubkeyIdentifier(@Nonnull PGPKeyRing keyRing, @Nonnull OpenPgpV4Fingerprint subkeyFingerprint) { - this(new OpenPgpV4Fingerprint(keyRing), subkeyFingerprint); - } - - /** - * Create a {@link SubkeyIdentifier} that identifies the primary key with the given fingerprint. - * This means, both {@link #getPrimaryKeyFingerprint()} and {@link #getSubkeyFingerprint()} return the same. - * - * @param primaryKeyFingerprint fingerprint of the identified key - */ - public SubkeyIdentifier(@Nonnull OpenPgpV4Fingerprint primaryKeyFingerprint) { - this(primaryKeyFingerprint, primaryKeyFingerprint); - } - - /** - * Create a {@link SubkeyIdentifier} which points to the subkey with the given subkeyFingerprint, - * which has a primary key with the given primaryKeyFingerprint. - * - * @param primaryKeyFingerprint fingerprint of the primary key - * @param subkeyFingerprint fingerprint of the subkey - */ - public SubkeyIdentifier(@Nonnull OpenPgpV4Fingerprint primaryKeyFingerprint, @Nonnull OpenPgpV4Fingerprint subkeyFingerprint) { - this.primaryKeyFingerprint = primaryKeyFingerprint; - this.subkeyFingerprint = subkeyFingerprint; - } - - public @Nonnull OpenPgpV4Fingerprint getFingerprint() { - return getSubkeyFingerprint(); - } - - public long getKeyId() { - return getSubkeyId(); - } - - /** - * Return the {@link OpenPgpV4Fingerprint} of the primary key of the identified key. - * This might be the same as {@link #getSubkeyFingerprint()} if the identified subkey is the primary key. - * - * @return primary key fingerprint - */ - public @Nonnull OpenPgpV4Fingerprint getPrimaryKeyFingerprint() { - return primaryKeyFingerprint; - } - - /** - * Return the key id of the primary key of the identified key. - * This might be the same as {@link #getSubkeyId()} if the identified subkey is the primary key. - * - * @return primary key id - */ - public long getPrimaryKeyId() { - return getPrimaryKeyFingerprint().getKeyId(); - } - - /** - * Return the {@link OpenPgpV4Fingerprint} of the identified subkey. - * - * @return subkey fingerprint - */ - public @Nonnull OpenPgpV4Fingerprint getSubkeyFingerprint() { - return subkeyFingerprint; - } - - /** - * Return the key id of the identified subkey. - * - * @return subkey id - */ - public long getSubkeyId() { - return getSubkeyFingerprint().getKeyId(); - } - - @Override - public int hashCode() { - return primaryKeyFingerprint.hashCode() * 31 + subkeyFingerprint.hashCode(); - } - - @Override - public boolean equals(Object obj) { - if (obj == null) { - return false; - } - if (this == obj) { - return true; - } - if (!(obj instanceof SubkeyIdentifier)) { - return false; - } - SubkeyIdentifier other = (SubkeyIdentifier) obj; - return getPrimaryKeyFingerprint().equals(other.getPrimaryKeyFingerprint()) - && getSubkeyFingerprint().equals(other.getSubkeyFingerprint()); - } - - @Override - public String toString() { - return getSubkeyFingerprint() + " " + getPrimaryKeyFingerprint(); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/collection/PGPKeyRingCollection.java b/pgpainless-core/src/main/java/org/pgpainless/key/collection/PGPKeyRingCollection.java deleted file mode 100644 index 352343b5..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/collection/PGPKeyRingCollection.java +++ /dev/null @@ -1,114 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub , 2021 Flowcrypt a.s. -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.collection; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import javax.annotation.Nonnull; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPKeyRing; -import org.bouncycastle.openpgp.PGPMarker; -import org.bouncycastle.openpgp.PGPObjectFactory; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; -import org.pgpainless.implementation.ImplementationFactory; -import org.pgpainless.util.ArmorUtils; - -/** - * This class describes a logic of handling a collection of different {@link PGPKeyRing}. The logic was inspired by - * {@link PGPSecretKeyRingCollection} and {@link PGPPublicKeyRingCollection}. - */ -public class PGPKeyRingCollection { - - private final PGPSecretKeyRingCollection pgpSecretKeyRingCollection; - private final PGPPublicKeyRingCollection pgpPublicKeyRingCollection; - - public PGPKeyRingCollection(@Nonnull byte[] encoding, boolean isSilent) throws IOException, PGPException { - this(new ByteArrayInputStream(encoding), isSilent); - } - - /** - * Build a {@link PGPKeyRingCollection} from the passed in input stream. - * - * @param in input stream containing data - * @param isSilent flag indicating that unsupported objects will be ignored - * @throws IOException if a problem parsing the base stream occurs - * @throws PGPException if an object is encountered which isn't a {@link PGPSecretKeyRing} or {@link PGPPublicKeyRing} - */ - public PGPKeyRingCollection(@Nonnull InputStream in, boolean isSilent) throws IOException, PGPException { - // Double getDecoderStream because of #96 - InputStream decoderStream = ArmorUtils.getDecoderStream(in); - PGPObjectFactory pgpFact = new PGPObjectFactory(decoderStream, ImplementationFactory.getInstance().getKeyFingerprintCalculator()); - Object obj; - - List secretKeyRings = new ArrayList<>(); - List publicKeyRings = new ArrayList<>(); - - while ((obj = pgpFact.nextObject()) != null) { - if (obj instanceof PGPMarker) { - // Skip marker packets - continue; - } - if (obj instanceof PGPSecretKeyRing) { - secretKeyRings.add((PGPSecretKeyRing) obj); - } else if (obj instanceof PGPPublicKeyRing) { - publicKeyRings.add((PGPPublicKeyRing) obj); - } else if (!isSilent) { - throw new PGPException(obj.getClass().getName() + " found where " + - PGPSecretKeyRing.class.getSimpleName() + " or " + - PGPPublicKeyRing.class.getSimpleName() + " expected"); - } - } - - pgpSecretKeyRingCollection = new PGPSecretKeyRingCollection(secretKeyRings); - pgpPublicKeyRingCollection = new PGPPublicKeyRingCollection(publicKeyRings); - } - - public PGPKeyRingCollection(@Nonnull Collection collection, boolean isSilent) - throws IOException, PGPException { - List secretKeyRings = new ArrayList<>(); - List publicKeyRings = new ArrayList<>(); - - for (PGPKeyRing pgpKeyRing : collection) { - if (pgpKeyRing instanceof PGPSecretKeyRing) { - secretKeyRings.add((PGPSecretKeyRing) pgpKeyRing); - } else if (pgpKeyRing instanceof PGPPublicKeyRing) { - publicKeyRings.add((PGPPublicKeyRing) pgpKeyRing); - } else if (!isSilent) { - throw new PGPException(pgpKeyRing.getClass().getName() + " found where " + - PGPSecretKeyRing.class.getSimpleName() + " or " + - PGPPublicKeyRing.class.getSimpleName() + " expected"); - } - } - - pgpSecretKeyRingCollection = new PGPSecretKeyRingCollection(secretKeyRings); - pgpPublicKeyRingCollection = new PGPPublicKeyRingCollection(publicKeyRings); - } - - public @Nonnull PGPSecretKeyRingCollection getPGPSecretKeyRingCollection() { - return pgpSecretKeyRingCollection; - } - - public @Nonnull PGPPublicKeyRingCollection getPgpPublicKeyRingCollection() { - return pgpPublicKeyRingCollection; - } - - /** - * Return the number of rings in this collection. - * - * @return total size of {@link PGPSecretKeyRingCollection} and {@link PGPPublicKeyRingCollection} - * in this collection - */ - public int size() { - return pgpSecretKeyRingCollection.size() + pgpPublicKeyRingCollection.size(); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/collection/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/key/collection/package-info.java deleted file mode 100644 index b2f5b153..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/collection/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * OpenPGP key collections. - */ -package org.pgpainless.key.collection; diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingBuilder.java deleted file mode 100644 index b5fe326d..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingBuilder.java +++ /dev/null @@ -1,430 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.generation; - - -import java.io.IOException; -import java.nio.charset.Charset; -import java.security.InvalidAlgorithmParameterException; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.NoSuchAlgorithmException; -import java.util.ArrayList; -import java.util.Date; -import java.util.Iterator; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Set; -import javax.annotation.Nonnull; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPKeyPair; -import org.bouncycastle.openpgp.PGPKeyRingGenerator; -import org.bouncycastle.openpgp.PGPPrivateKey; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.PGPSignatureGenerator; -import org.bouncycastle.openpgp.PGPSignatureSubpacketGenerator; -import org.bouncycastle.openpgp.PGPSignatureSubpacketVector; -import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; -import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; -import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder; -import org.bouncycastle.openpgp.operator.PGPDigestCalculator; -import org.pgpainless.PGPainless; -import org.pgpainless.algorithm.HashAlgorithm; -import org.pgpainless.algorithm.KeyFlag; -import org.pgpainless.algorithm.SignatureType; -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; -import org.pgpainless.implementation.ImplementationFactory; -import org.pgpainless.key.generation.type.KeyType; -import org.pgpainless.key.generation.type.eddsa.EdDSACurve; -import org.pgpainless.key.generation.type.rsa.RsaLength; -import org.pgpainless.key.generation.type.xdh.XDHSpec; -import org.pgpainless.key.protection.UnlockSecretKey; -import org.pgpainless.key.util.UserId; -import org.pgpainless.provider.ProviderFactory; -import org.pgpainless.util.Passphrase; -import org.pgpainless.signature.subpackets.SignatureSubpacketGeneratorUtil; - -public class KeyRingBuilder implements KeyRingBuilderInterface { - - private final Charset UTF8 = Charset.forName("UTF-8"); - - private PGPSignatureGenerator signatureGenerator; - private PGPDigestCalculator digestCalculator; - private PBESecretKeyEncryptor secretKeyEncryptor; - - private KeySpec primaryKeySpec; - private final List subkeySpecs = new ArrayList<>(); - private final Set userIds = new LinkedHashSet<>(); - private Passphrase passphrase = null; - private Date expirationDate = null; - - /** - * Creates a simple, unencrypted RSA KeyPair of length {@code length} with user-id {@code userId}. - * The KeyPair consists of a single RSA master key which is used for signing, encryption and certification. - * - * @param userId user id. - * @param length length in bits. - * - * @return {@link PGPSecretKeyRing} containing the KeyPair. - */ - public PGPSecretKeyRing simpleRsaKeyRing(@Nonnull UserId userId, @Nonnull RsaLength length) - throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { - return simpleRsaKeyRing(userId.toString(), length); - } - - /** - * Creates a simple, unencrypted RSA KeyPair of length {@code length} with user-id {@code userId}. - * The KeyPair consists of a single RSA master key which is used for signing, encryption and certification. - * - * @param userId user id. - * @param length length in bits. - * - * @return {@link PGPSecretKeyRing} containing the KeyPair. - */ - public PGPSecretKeyRing simpleRsaKeyRing(@Nonnull String userId, @Nonnull RsaLength length) - throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { - return simpleRsaKeyRing(userId, length, null); - } - - /** - * Creates a simple RSA KeyPair of length {@code length} with user-id {@code userId}. - * The KeyPair consists of a single RSA master key which is used for signing, encryption and certification. - * - * @param userId user id. - * @param length length in bits. - * @param password Password of the key. Can be null for unencrypted keys. - * - * @return {@link PGPSecretKeyRing} containing the KeyPair. - */ - public PGPSecretKeyRing simpleRsaKeyRing(@Nonnull UserId userId, @Nonnull RsaLength length, String password) - throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { - return simpleRsaKeyRing(userId.toString(), length, password); - } - - /** - * Creates a simple RSA KeyPair of length {@code length} with user-id {@code userId}. - * The KeyPair consists of a single RSA master key which is used for signing, encryption and certification. - * - * @param userId user id. - * @param length length in bits. - * @param password Password of the key. Can be null for unencrypted keys. - * - * @return {@link PGPSecretKeyRing} containing the KeyPair. - */ - public PGPSecretKeyRing simpleRsaKeyRing(@Nonnull String userId, @Nonnull RsaLength length, String password) - throws PGPException, NoSuchAlgorithmException, InvalidAlgorithmParameterException { - KeyRingBuilder builder = new KeyRingBuilder() - .setPrimaryKey(KeySpec.getBuilder(KeyType.RSA(length), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA, KeyFlag.ENCRYPT_COMMS)) - .addUserId(userId); - - if (!isNullOrEmpty(password)) { - builder.setPassphrase(Passphrase.fromPassword(password)); - } - return builder.build(); - } - - /** - * Creates a key ring consisting of an ed25519 EdDSA primary key and a curve25519 XDH subkey. - * The EdDSA primary key is used for signing messages and certifying the sub key. - * The XDH subkey is used for encryption and decryption of messages. - * - * @param userId user-id - * - * @return {@link PGPSecretKeyRing} containing the key pairs. - */ - public PGPSecretKeyRing simpleEcKeyRing(@Nonnull UserId userId) - throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { - return simpleEcKeyRing(userId.toString()); - } - - /** - * Creates a key ring consisting of an ed25519 EdDSA primary key and a curve25519 XDH subkey. - * The EdDSA primary key is used for signing messages and certifying the sub key. - * The XDH subkey is used for encryption and decryption of messages. - * - * @param userId user-id - * - * @return {@link PGPSecretKeyRing} containing the key pairs. - */ - public PGPSecretKeyRing simpleEcKeyRing(@Nonnull String userId) - throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { - return simpleEcKeyRing(userId, null); - } - - /** - * Creates a key ring consisting of an ed25519 EdDSA primary key and a curve25519 XDH subkey. - * The EdDSA primary key is used for signing messages and certifying the sub key. - * The XDH subkey is used for encryption and decryption of messages. - * - * @param userId user-id - * @param password Password of the private key. Can be null for an unencrypted key. - * - * @return {@link PGPSecretKeyRing} containing the key pairs. - */ - public PGPSecretKeyRing simpleEcKeyRing(@Nonnull UserId userId, String password) - throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { - return simpleEcKeyRing(userId.toString(), password); - } - - /** - * Creates a key ring consisting of an ed25519 EdDSA primary key and a X25519 XDH subkey. - * The EdDSA primary key is used for signing messages and certifying the sub key. - * The XDH subkey is used for encryption and decryption of messages. - * - * @param userId user-id - * @param password Password of the private key. Can be null for an unencrypted key. - * - * @return {@link PGPSecretKeyRing} containing the key pairs. - */ - public PGPSecretKeyRing simpleEcKeyRing(@Nonnull String userId, String password) - throws PGPException, NoSuchAlgorithmException, InvalidAlgorithmParameterException { - KeyRingBuilder builder = new KeyRingBuilder() - .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA)) - .addSubkey(KeySpec.getBuilder(KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_STORAGE, KeyFlag.ENCRYPT_COMMS)) - .addUserId(userId); - - if (!isNullOrEmpty(password)) { - builder.setPassphrase(Passphrase.fromPassword(password)); - } - return builder.build(); - } - - /** - * Generate a modern PGP key ring consisting of an ed25519 EdDSA primary key which is used to certify - * an X25519 XDH encryption subkey and an ed25519 EdDSA signing key. - * - * @param userId primary user id - * @param password passphrase or null if the key should be unprotected. - * @return key ring - */ - public PGPSecretKeyRing modernKeyRing(String userId, String password) - throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { - KeyRingBuilder builder = new KeyRingBuilder() - .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER)) - .addSubkey(KeySpec.getBuilder(KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_STORAGE, KeyFlag.ENCRYPT_COMMS)) - .addSubkey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.SIGN_DATA)) - .addUserId(userId); - if (!isNullOrEmpty(password)) { - builder.setPassphrase(Passphrase.fromPassword(password)); - } - return builder.build(); - } - - @Override - public KeyRingBuilder setPrimaryKey(@Nonnull KeySpec keySpec) { - verifyMasterKeyCanCertify(keySpec); - this.primaryKeySpec = keySpec; - return this; - } - - @Override - public KeyRingBuilder addSubkey(@Nonnull KeySpec keySpec) { - this.subkeySpecs.add(keySpec); - return this; - } - - @Override - public KeyRingBuilder addUserId(@Nonnull String userId) { - this.userIds.add(userId.trim()); - return this; - } - - @Override - public KeyRingBuilder addUserId(@Nonnull byte[] userId) { - return addUserId(new String(userId, UTF8)); - } - - @Override - public KeyRingBuilder setExpirationDate(@Nonnull Date expirationDate) { - Date now = new Date(); - if (now.after(expirationDate)) { - throw new IllegalArgumentException("Expiration date must be in the future."); - } - this.expirationDate = expirationDate; - return this; - } - - @Override - public KeyRingBuilder setPassphrase(@Nonnull Passphrase passphrase) { - this.passphrase = passphrase; - return this; - } - - private static boolean isNullOrEmpty(String password) { - return password == null || password.trim().isEmpty(); - } - - private void verifyMasterKeyCanCertify(KeySpec spec) { - if (!hasCertifyOthersFlag(spec)) { - throw new IllegalArgumentException("Certification Key MUST have KeyFlag CERTIFY_OTHER"); - } - if (!keyIsCertificationCapable(spec)) { - throw new IllegalArgumentException("Key algorithm " + spec.getKeyType().getName() + " is not capable of creating certifications."); - } - } - - private boolean hasCertifyOthersFlag(KeySpec keySpec) { - return SignatureSubpacketGeneratorUtil.hasKeyFlag(KeyFlag.CERTIFY_OTHER, keySpec.getSubpacketGenerator()); - } - - private boolean keyIsCertificationCapable(KeySpec keySpec) { - return keySpec.getKeyType().canCertify(); - } - - @Override - public PGPSecretKeyRing build() throws NoSuchAlgorithmException, PGPException, - InvalidAlgorithmParameterException { - if (userIds.isEmpty()) { - throw new IllegalStateException("At least one user-id is required."); - } - digestCalculator = buildDigestCalculator(); - secretKeyEncryptor = buildSecretKeyEncryptor(); - PBESecretKeyDecryptor secretKeyDecryptor = buildSecretKeyDecryptor(); - - if (passphrase != null) { - passphrase.clear(); - } - - // Generate Primary Key - PGPKeyPair certKey = generateKeyPair(primaryKeySpec); - PGPContentSignerBuilder signer = buildContentSigner(certKey); - signatureGenerator = new PGPSignatureGenerator(signer); - PGPSignatureSubpacketGenerator hashedSubPacketGenerator = primaryKeySpec.getSubpacketGenerator(); - hashedSubPacketGenerator.setPrimaryUserID(false, true); - if (expirationDate != null) { - SignatureSubpacketGeneratorUtil.setExpirationDateInSubpacketGenerator( - expirationDate, new Date(), hashedSubPacketGenerator); - } - PGPSignatureSubpacketVector hashedSubPackets = hashedSubPacketGenerator.generate(); - - // Generator which the user can get the key pair from - PGPKeyRingGenerator ringGenerator = buildRingGenerator(certKey, signer, hashedSubPackets); - - addSubKeys(certKey, ringGenerator); - - // Generate secret key ring with only primary user id - PGPSecretKeyRing secretKeyRing = ringGenerator.generateSecretKeyRing(); - - Iterator secretKeys = secretKeyRing.getSecretKeys(); - - // Attempt to add additional user-ids to the primary public key - PGPPublicKey primaryPubKey = secretKeys.next().getPublicKey(); - PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKeyRing.getSecretKey(), secretKeyDecryptor); - Iterator additionalUserIds = userIds.iterator(); - additionalUserIds.next(); // Skip primary user id - while (additionalUserIds.hasNext()) { - String additionalUserId = additionalUserIds.next(); - signatureGenerator.init(SignatureType.POSITIVE_CERTIFICATION.getCode(), privateKey); - PGPSignature additionalUserIdSignature = - signatureGenerator.generateCertification(additionalUserId, primaryPubKey); - primaryPubKey = PGPPublicKey.addCertification(primaryPubKey, - additionalUserId, additionalUserIdSignature); - } - - // "reassemble" secret key ring with modified primary key - PGPSecretKey primarySecKey = new PGPSecretKey( - privateKey, - primaryPubKey, digestCalculator, true, secretKeyEncryptor); - List secretKeyList = new ArrayList<>(); - secretKeyList.add(primarySecKey); - while (secretKeys.hasNext()) { - secretKeyList.add(secretKeys.next()); - } - secretKeyRing = new PGPSecretKeyRing(secretKeyList); - - return secretKeyRing; - } - - private PGPKeyRingGenerator buildRingGenerator(PGPKeyPair certKey, - PGPContentSignerBuilder signer, - PGPSignatureSubpacketVector hashedSubPackets) - throws PGPException { - String primaryUserId = userIds.iterator().next(); - return new PGPKeyRingGenerator( - SignatureType.POSITIVE_CERTIFICATION.getCode(), certKey, - primaryUserId, digestCalculator, - hashedSubPackets, null, signer, secretKeyEncryptor); - } - - private void addSubKeys(PGPKeyPair primaryKey, PGPKeyRingGenerator ringGenerator) - throws NoSuchAlgorithmException, PGPException, InvalidAlgorithmParameterException { - for (KeySpec subKeySpec : subkeySpecs) { - PGPKeyPair subKey = generateKeyPair(subKeySpec); - if (subKeySpec.isInheritedSubPackets()) { - ringGenerator.addSubKey(subKey); - } else { - PGPSignatureSubpacketVector hashedSubpackets = subKeySpec.getSubpackets(); - try { - hashedSubpackets = addPrimaryKeyBindingSignatureIfNecessary(primaryKey, subKey, hashedSubpackets); - } catch (IOException e) { - throw new PGPException("Exception while adding primary key binding signature to signing subkey", e); - } - ringGenerator.addSubKey(subKey, hashedSubpackets, null); - } - } - } - - private PGPSignatureSubpacketVector addPrimaryKeyBindingSignatureIfNecessary(PGPKeyPair primaryKey, PGPKeyPair subKey, PGPSignatureSubpacketVector hashedSubpackets) throws PGPException, IOException { - int keyFlagMask = hashedSubpackets.getKeyFlags(); - if (!KeyFlag.hasKeyFlag(keyFlagMask, KeyFlag.SIGN_DATA) && !KeyFlag.hasKeyFlag(keyFlagMask, KeyFlag.CERTIFY_OTHER)) { - return hashedSubpackets; - } - - PGPSignatureGenerator bindingSignatureGenerator = new PGPSignatureGenerator(buildContentSigner(subKey)); - bindingSignatureGenerator.init(SignatureType.PRIMARYKEY_BINDING.getCode(), subKey.getPrivateKey()); - PGPSignature primaryKeyBindingSig = bindingSignatureGenerator.generateCertification(primaryKey.getPublicKey(), subKey.getPublicKey()); - PGPSignatureSubpacketGenerator subpacketGenerator = new PGPSignatureSubpacketGenerator(hashedSubpackets); - subpacketGenerator.addEmbeddedSignature(false, primaryKeyBindingSig); - return subpacketGenerator.generate(); - } - - private PGPContentSignerBuilder buildContentSigner(PGPKeyPair certKey) { - HashAlgorithm hashAlgorithm = PGPainless.getPolicy().getSignatureHashAlgorithmPolicy().defaultHashAlgorithm(); - return ImplementationFactory.getInstance().getPGPContentSignerBuilder( - certKey.getPublicKey().getAlgorithm(), - hashAlgorithm.getAlgorithmId()); - } - - private PBESecretKeyEncryptor buildSecretKeyEncryptor() { - SymmetricKeyAlgorithm keyEncryptionAlgorithm = PGPainless.getPolicy().getSymmetricKeyEncryptionAlgorithmPolicy() - .getDefaultSymmetricKeyAlgorithm(); - PBESecretKeyEncryptor encryptor = passphrase == null || passphrase.isEmpty() ? - null : // unencrypted key pair, otherwise AES-256 encrypted - ImplementationFactory.getInstance().getPBESecretKeyEncryptor( - keyEncryptionAlgorithm, digestCalculator, passphrase); - return encryptor; - } - - private PBESecretKeyDecryptor buildSecretKeyDecryptor() throws PGPException { - PBESecretKeyDecryptor decryptor = passphrase == null || passphrase.isEmpty() ? - null : - ImplementationFactory.getInstance().getPBESecretKeyDecryptor(passphrase); - return decryptor; - } - - private PGPDigestCalculator buildDigestCalculator() throws PGPException { - return ImplementationFactory.getInstance().getPGPDigestCalculator(HashAlgorithm.SHA1); - } - - public static PGPKeyPair generateKeyPair(KeySpec spec) - throws NoSuchAlgorithmException, PGPException, - InvalidAlgorithmParameterException { - KeyType type = spec.getKeyType(); - KeyPairGenerator certKeyGenerator = KeyPairGenerator.getInstance(type.getName(), - ProviderFactory.getProvider()); - certKeyGenerator.initialize(type.getAlgorithmSpec()); - - // Create raw Key Pair - KeyPair keyPair = certKeyGenerator.generateKeyPair(); - - // Form PGP key pair - PGPKeyPair pgpKeyPair = ImplementationFactory.getInstance().getPGPKeyPair(type.getAlgorithm(), keyPair, new Date()); - return pgpKeyPair; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingBuilderInterface.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingBuilderInterface.java deleted file mode 100644 index ecff123b..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingBuilderInterface.java +++ /dev/null @@ -1,45 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.generation; - -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; -import java.util.Date; -import javax.annotation.Nonnull; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.pgpainless.key.util.UserId; -import org.pgpainless.util.Passphrase; - -public interface KeyRingBuilderInterface> { - - B setPrimaryKey(@Nonnull KeySpec keySpec); - - default B setPrimaryKey(@Nonnull KeySpecBuilder builder) { - return setPrimaryKey(builder.build()); - } - - B addSubkey(@Nonnull KeySpec keySpec); - - default B addSubkey(@Nonnull KeySpecBuilder builder) { - return addSubkey(builder.build()); - } - - default B addUserId(UserId userId) { - return addUserId(userId.toString()); - } - - B addUserId(@Nonnull String userId); - - B addUserId(@Nonnull byte[] userId); - - B setExpirationDate(@Nonnull Date expirationDate); - - B setPassphrase(@Nonnull Passphrase passphrase); - - PGPSecretKeyRing build() throws NoSuchAlgorithmException, PGPException, - InvalidAlgorithmParameterException; -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeySpec.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeySpec.java deleted file mode 100644 index 364384ae..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeySpec.java +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.generation; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import org.bouncycastle.openpgp.PGPSignatureSubpacketGenerator; -import org.bouncycastle.openpgp.PGPSignatureSubpacketVector; -import org.pgpainless.algorithm.KeyFlag; -import org.pgpainless.key.generation.type.KeyType; - -public class KeySpec { - - private final KeyType keyType; - private final PGPSignatureSubpacketGenerator subpacketGenerator; - private final boolean inheritedSubPackets; - - KeySpec(@Nonnull KeyType type, - @Nullable PGPSignatureSubpacketGenerator subpacketGenerator, - boolean inheritedSubPackets) { - this.keyType = type; - this.subpacketGenerator = subpacketGenerator; - this.inheritedSubPackets = inheritedSubPackets; - } - - @Nonnull - KeyType getKeyType() { - return keyType; - } - - @Nullable - public PGPSignatureSubpacketVector getSubpackets() { - return subpacketGenerator != null ? subpacketGenerator.generate() : null; - } - - PGPSignatureSubpacketGenerator getSubpacketGenerator() { - return subpacketGenerator; - } - - boolean isInheritedSubPackets() { - return inheritedSubPackets; - } - - public static KeySpecBuilder getBuilder(KeyType type, KeyFlag flag, KeyFlag... flags) { - return new KeySpecBuilder(type, flag, flags); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeySpecBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeySpecBuilder.java deleted file mode 100644 index 80756b74..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeySpecBuilder.java +++ /dev/null @@ -1,127 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.generation; - -import java.util.Arrays; -import java.util.Iterator; -import java.util.LinkedHashSet; -import java.util.Set; -import javax.annotation.Nonnull; - -import org.bouncycastle.openpgp.PGPSignatureSubpacketGenerator; -import org.pgpainless.PGPainless; -import org.pgpainless.algorithm.AlgorithmSuite; -import org.pgpainless.algorithm.CompressionAlgorithm; -import org.pgpainless.algorithm.Feature; -import org.pgpainless.algorithm.HashAlgorithm; -import org.pgpainless.algorithm.KeyFlag; -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; -import org.pgpainless.key.generation.type.KeyType; -import org.pgpainless.util.CollectionUtils; - -public class KeySpecBuilder implements KeySpecBuilderInterface { - - private final KeyType type; - private final KeyFlag[] keyFlags; - private final PGPSignatureSubpacketGenerator hashedSubPackets = new PGPSignatureSubpacketGenerator(); - private final AlgorithmSuite algorithmSuite = PGPainless.getPolicy().getKeyGenerationAlgorithmSuite(); - private Set preferredCompressionAlgorithms = algorithmSuite.getCompressionAlgorithms(); - private Set preferredHashAlgorithms = algorithmSuite.getHashAlgorithms(); - private Set preferredSymmetricAlgorithms = algorithmSuite.getSymmetricKeyAlgorithms(); - - KeySpecBuilder(@Nonnull KeyType type, KeyFlag flag, KeyFlag... flags) { - if (flag == null) { - throw new IllegalArgumentException("Key MUST carry at least one key flag"); - } - if (flags == null) { - throw new IllegalArgumentException("List of additional flags MUST NOT be null."); - } - flags = CollectionUtils.concat(flag, flags); - assureKeyCanCarryFlags(type, flags); - this.type = type; - this.keyFlags = flags; - } - - @Override - public KeySpecBuilder overridePreferredCompressionAlgorithms(@Nonnull CompressionAlgorithm... compressionAlgorithms) { - this.preferredCompressionAlgorithms = new LinkedHashSet<>(Arrays.asList(compressionAlgorithms)); - return this; - } - - @Override - public KeySpecBuilder overridePreferredHashAlgorithms(@Nonnull HashAlgorithm... preferredHashAlgorithms) { - this.preferredHashAlgorithms = new LinkedHashSet<>(Arrays.asList(preferredHashAlgorithms)); - return this; - } - - @Override - public KeySpecBuilder overridePreferredSymmetricKeyAlgorithms(@Nonnull SymmetricKeyAlgorithm... preferredSymmetricKeyAlgorithms) { - this.preferredSymmetricAlgorithms = new LinkedHashSet<>(Arrays.asList(preferredSymmetricKeyAlgorithms)); - return this; - } - - - @Override - public KeySpec build() { - this.hashedSubPackets.setKeyFlags(false, KeyFlag.toBitmask(keyFlags)); - this.hashedSubPackets.setPreferredCompressionAlgorithms(false, getPreferredCompressionAlgorithmIDs()); - this.hashedSubPackets.setPreferredHashAlgorithms(false, getPreferredHashAlgorithmIDs()); - this.hashedSubPackets.setPreferredSymmetricAlgorithms(false, getPreferredSymmetricKeyAlgorithmIDs()); - this.hashedSubPackets.setFeature(false, Feature.MODIFICATION_DETECTION.getFeatureId()); - - return new KeySpec(type, hashedSubPackets, false); - } - - private int[] getPreferredCompressionAlgorithmIDs() { - int[] ids = new int[preferredCompressionAlgorithms.size()]; - Iterator iterator = preferredCompressionAlgorithms.iterator(); - for (int i = 0; i < ids.length; i++) { - ids[i] = iterator.next().getAlgorithmId(); - } - return ids; - } - - private int[] getPreferredHashAlgorithmIDs() { - int[] ids = new int[preferredHashAlgorithms.size()]; - Iterator iterator = preferredHashAlgorithms.iterator(); - for (int i = 0; i < ids.length; i++) { - ids[i] = iterator.next().getAlgorithmId(); - } - return ids; - } - - private int[] getPreferredSymmetricKeyAlgorithmIDs() { - int[] ids = new int[preferredSymmetricAlgorithms.size()]; - Iterator iterator = preferredSymmetricAlgorithms.iterator(); - for (int i = 0; i < ids.length; i++) { - ids[i] = iterator.next().getAlgorithmId(); - } - return ids; - } - - private static void assureKeyCanCarryFlags(KeyType type, KeyFlag... flags) { - final int mask = KeyFlag.toBitmask(flags); - - if (!type.canCertify() && KeyFlag.hasKeyFlag(mask, KeyFlag.CERTIFY_OTHER)) { - throw new IllegalArgumentException("KeyType " + type.getName() + " cannot carry key flag CERTIFY_OTHER."); - } - - if (!type.canSign() && KeyFlag.hasKeyFlag(mask, KeyFlag.SIGN_DATA)) { - throw new IllegalArgumentException("KeyType " + type.getName() + " cannot carry key flag SIGN_DATA."); - } - - if (!type.canEncryptCommunication() && KeyFlag.hasKeyFlag(mask, KeyFlag.ENCRYPT_COMMS)) { - throw new IllegalArgumentException("KeyType " + type.getName() + " cannot carry key flag ENCRYPT_COMMS."); - } - - if (!type.canEncryptStorage() && KeyFlag.hasKeyFlag(mask, KeyFlag.ENCRYPT_STORAGE)) { - throw new IllegalArgumentException("KeyType " + type.getName() + " cannot carry key flag ENCRYPT_STORAGE."); - } - - if (!type.canAuthenticate() && KeyFlag.hasKeyFlag(mask, KeyFlag.AUTHENTICATION)) { - throw new IllegalArgumentException("KeyType " + type.getName() + " cannot carry key flag AUTHENTIACTION."); - } - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeySpecBuilderInterface.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeySpecBuilderInterface.java deleted file mode 100644 index cd68e8b4..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeySpecBuilderInterface.java +++ /dev/null @@ -1,22 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.generation; - -import javax.annotation.Nonnull; - -import org.pgpainless.algorithm.CompressionAlgorithm; -import org.pgpainless.algorithm.HashAlgorithm; -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; - -public interface KeySpecBuilderInterface { - - KeySpecBuilder overridePreferredCompressionAlgorithms(@Nonnull CompressionAlgorithm... compressionAlgorithms); - - KeySpecBuilder overridePreferredHashAlgorithms(@Nonnull HashAlgorithm... preferredHashAlgorithms); - - KeySpecBuilder overridePreferredSymmetricKeyAlgorithms(@Nonnull SymmetricKeyAlgorithm... preferredSymmetricKeyAlgorithms); - - KeySpec build(); -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/package-info.java deleted file mode 100644 index 96728227..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Classes related to OpenPGP key generation. - */ -package org.pgpainless.key.generation; diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/KeyLength.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/KeyLength.java deleted file mode 100644 index 1cadfef8..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/KeyLength.java +++ /dev/null @@ -1,10 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.generation.type; - -public interface KeyLength { - - int getLength(); -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/KeyType.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/KeyType.java deleted file mode 100644 index 584ec1e7..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/KeyType.java +++ /dev/null @@ -1,112 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.generation.type; - -import java.security.spec.AlgorithmParameterSpec; - -import org.pgpainless.algorithm.PublicKeyAlgorithm; -import org.pgpainless.key.generation.type.ecc.EllipticCurve; -import org.pgpainless.key.generation.type.ecc.ecdh.ECDH; -import org.pgpainless.key.generation.type.ecc.ecdsa.ECDSA; -import org.pgpainless.key.generation.type.eddsa.EdDSA; -import org.pgpainless.key.generation.type.eddsa.EdDSACurve; -import org.pgpainless.key.generation.type.rsa.RsaLength; -import org.pgpainless.key.generation.type.rsa.RSA; -import org.pgpainless.key.generation.type.xdh.XDH; -import org.pgpainless.key.generation.type.xdh.XDHSpec; - -public interface KeyType { - - /** - * Return the encryption algorithm name. - * - * @return algorithm name. - */ - String getName(); - - /** - * Return the public key algorithm. - * - * @return public key algorithm - */ - PublicKeyAlgorithm getAlgorithm(); - - /** - * Return an implementation of {@link AlgorithmParameterSpec} that can be used to generate the key. - * - * @return algorithm parameter spec - */ - AlgorithmParameterSpec getAlgorithmSpec(); - - /** - * Return true if the key that is generated from this type is able to carry the SIGN_DATA key flag. - * See {@link org.pgpainless.algorithm.KeyFlag#SIGN_DATA}. - * - * @return true if the key can sign. - */ - default boolean canSign() { - return getAlgorithm().isSigningCapable(); - } - - /** - * Return true if the key that is generated from this type is able to carry the CERTIFY_OTHER key flag. - * See {@link org.pgpainless.algorithm.KeyFlag#CERTIFY_OTHER}. - * - * @return true if the key is able to certify other keys - */ - default boolean canCertify() { - return canSign(); - } - - /** - * Return true if the key that is generated from this type is able to carry the AUTHENTICATION key flag. - * See {@link org.pgpainless.algorithm.KeyFlag#AUTHENTICATION}. - * - * @return true if the key is able to be used for authentication purposes. - */ - default boolean canAuthenticate() { - return canSign(); - } - - /** - * Return true if the key that is generated from this type is able to carry the ENCRYPT_COMMS key flag. - * See {@link org.pgpainless.algorithm.KeyFlag#ENCRYPT_COMMS}. - * - * @return true if the key can encrypt communication - */ - default boolean canEncryptCommunication() { - return getAlgorithm().isEncryptionCapable(); - } - - /** - * Return true if the key that is generated from this type is able to carry the ENCRYPT_STORAGE key flag. - * See {@link org.pgpainless.algorithm.KeyFlag#ENCRYPT_STORAGE}. - * - * @return true if the key can encrypt for storage - */ - default boolean canEncryptStorage() { - return getAlgorithm().isEncryptionCapable(); - } - - static KeyType RSA(RsaLength length) { - return RSA.withLength(length); - } - - static KeyType ECDH(EllipticCurve curve) { - return ECDH.fromCurve(curve); - } - - static KeyType ECDSA(EllipticCurve curve) { - return ECDSA.fromCurve(curve); - } - - static KeyType EDDSA(EdDSACurve curve) { - return EdDSA.fromCurve(curve); - } - - static KeyType XDH(XDHSpec curve) { - return XDH.fromSpec(curve); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/ecc/EllipticCurve.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/ecc/EllipticCurve.java deleted file mode 100644 index 851d2d32..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/ecc/EllipticCurve.java +++ /dev/null @@ -1,36 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.generation.type.ecc; - -import javax.annotation.Nonnull; - -import org.pgpainless.key.generation.type.xdh.XDHSpec; - -/** - * Elliptic curves for use with - * {@link org.pgpainless.key.generation.type.ecc.ecdh.ECDH}/{@link org.pgpainless.key.generation.type.ecc.ecdsa.ECDSA}. - * For curve25519 related curve definitions see - * {@link XDHSpec} and {@link org.pgpainless.key.generation.type.eddsa.EdDSACurve}. - */ -public enum EllipticCurve { - _P256("prime256v1"), // prime256v1 is equivalent to P-256, see https://tools.ietf.org/search/rfc4492#page-32 - _P384("secp384r1"), // secp384r1 is equivalent to P-384, see https://tools.ietf.org/search/rfc4492#page-32 - _P521("secp521r1"), // secp521r1 is equivalent to P-521, see https://tools.ietf.org/search/rfc4492#page-32 - _SECP256K1("secp256k1"), - _BRAINPOOLP256R1("brainpoolP256r1"), - _BRAINPOOLP384R1("brainpoolP384r1"), - _BRAINPOOLP512R1("brainpoolP512r1") - ; - - private final String name; - - EllipticCurve(@Nonnull String name) { - this.name = name; - } - - public String getName() { - return name; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/ecc/ecdh/ECDH.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/ecc/ecdh/ECDH.java deleted file mode 100644 index 53f0d1b7..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/ecc/ecdh/ECDH.java +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.generation.type.ecc.ecdh; - -import javax.annotation.Nonnull; -import java.security.spec.AlgorithmParameterSpec; - -import org.bouncycastle.jce.spec.ECNamedCurveGenParameterSpec; -import org.pgpainless.algorithm.PublicKeyAlgorithm; -import org.pgpainless.key.generation.type.KeyType; -import org.pgpainless.key.generation.type.ecc.EllipticCurve; - -public final class ECDH implements KeyType { - - private final EllipticCurve curve; - - private ECDH(EllipticCurve curve) { - this.curve = curve; - } - - public static ECDH fromCurve(@Nonnull EllipticCurve curve) { - return new ECDH(curve); - } - - @Override - public String getName() { - return "ECDH"; - } - - @Override - public PublicKeyAlgorithm getAlgorithm() { - return PublicKeyAlgorithm.ECDH; - } - - @Override - public AlgorithmParameterSpec getAlgorithmSpec() { - return new ECNamedCurveGenParameterSpec(curve.getName()); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/ecc/ecdh/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/ecc/ecdh/package-info.java deleted file mode 100644 index b1f2c882..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/ecc/ecdh/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Classes related to ECDH. - */ -package org.pgpainless.key.generation.type.ecc.ecdh; diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/ecc/ecdsa/ECDSA.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/ecc/ecdsa/ECDSA.java deleted file mode 100644 index 93c6398b..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/ecc/ecdsa/ECDSA.java +++ /dev/null @@ -1,43 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.generation.type.ecc.ecdsa; - - -import java.security.spec.AlgorithmParameterSpec; -import javax.annotation.Nonnull; - -import org.bouncycastle.jce.spec.ECNamedCurveGenParameterSpec; -import org.pgpainless.algorithm.PublicKeyAlgorithm; -import org.pgpainless.key.generation.type.ecc.EllipticCurve; -import org.pgpainless.key.generation.type.KeyType; - -public final class ECDSA implements KeyType { - - private final EllipticCurve curve; - - private ECDSA(@Nonnull EllipticCurve curve) { - this.curve = curve; - } - - public static ECDSA fromCurve(@Nonnull EllipticCurve curve) { - return new ECDSA(curve); - } - - @Override - public String getName() { - return "ECDSA"; - } - - @Override - public PublicKeyAlgorithm getAlgorithm() { - return PublicKeyAlgorithm.ECDSA; - } - - @Override - public AlgorithmParameterSpec getAlgorithmSpec() { - return new ECNamedCurveGenParameterSpec(curve.getName()); - } - -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/ecc/ecdsa/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/ecc/ecdsa/package-info.java deleted file mode 100644 index 9b8ca577..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/ecc/ecdsa/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Classes related to ECDSA. - */ -package org.pgpainless.key.generation.type.ecc.ecdsa; diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/ecc/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/ecc/package-info.java deleted file mode 100644 index d55a487a..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/ecc/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Classes describing different OpenPGP key types based on elliptic curves. - */ -package org.pgpainless.key.generation.type.ecc; diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/eddsa/EdDSA.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/eddsa/EdDSA.java deleted file mode 100644 index 67532d6c..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/eddsa/EdDSA.java +++ /dev/null @@ -1,43 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.generation.type.eddsa; - -import java.security.spec.AlgorithmParameterSpec; - -import org.bouncycastle.jce.spec.ECNamedCurveGenParameterSpec; -import org.pgpainless.algorithm.PublicKeyAlgorithm; -import org.pgpainless.key.generation.type.KeyType; - -/** - * Edwards-curve Digital Signature Algorithm (EdDSA). - */ -public final class EdDSA implements KeyType { - - private final EdDSACurve curve; - - private EdDSA(EdDSACurve curve) { - this.curve = curve; - } - - public static EdDSA fromCurve(EdDSACurve curve) { - return new EdDSA(curve); - } - - @Override - public String getName() { - return "EdDSA"; - } - - @Override - public PublicKeyAlgorithm getAlgorithm() { - return PublicKeyAlgorithm.EDDSA; - } - - @Override - public AlgorithmParameterSpec getAlgorithmSpec() { - return new ECNamedCurveGenParameterSpec(curve.getName()); - } - -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/eddsa/EdDSACurve.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/eddsa/EdDSACurve.java deleted file mode 100644 index cc1c1831..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/eddsa/EdDSACurve.java +++ /dev/null @@ -1,22 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.generation.type.eddsa; - -import javax.annotation.Nonnull; - -public enum EdDSACurve { - _Ed25519("ed25519"), - ; - - final String name; - - EdDSACurve(@Nonnull String curveName) { - this.name = curveName; - } - - public String getName() { - return name; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/eddsa/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/eddsa/package-info.java deleted file mode 100644 index cba16f54..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/eddsa/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Classes related to EdDSA. - */ -package org.pgpainless.key.generation.type.eddsa; diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/elgamal/ElGamal.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/elgamal/ElGamal.java deleted file mode 100644 index ac7a239b..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/elgamal/ElGamal.java +++ /dev/null @@ -1,44 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.generation.type.elgamal; - -import java.security.spec.AlgorithmParameterSpec; -import javax.annotation.Nonnull; - -import org.bouncycastle.jce.spec.ElGamalParameterSpec; -import org.pgpainless.algorithm.PublicKeyAlgorithm; -import org.pgpainless.key.generation.type.KeyType; - -/** - * ElGamal encryption only key type. - */ -public final class ElGamal implements KeyType { - - private final ElGamalLength length; - - private ElGamal(@Nonnull ElGamalLength length) { - this.length = length; - } - - public static ElGamal withLength(ElGamalLength length) { - return new ElGamal(length); - } - - @Override - public String getName() { - return "ElGamal"; - } - - @Override - public PublicKeyAlgorithm getAlgorithm() { - return PublicKeyAlgorithm.ELGAMAL_ENCRYPT; - } - - @Override - public AlgorithmParameterSpec getAlgorithmSpec() { - return new ElGamalParameterSpec(length.getP(), length.getG()); - } - -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/elgamal/ElGamalLength.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/elgamal/ElGamalLength.java deleted file mode 100644 index 852ad6f7..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/elgamal/ElGamalLength.java +++ /dev/null @@ -1,78 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.generation.type.elgamal; - -import java.math.BigInteger; - -import org.pgpainless.key.generation.type.KeyLength; - -/** - * The following primes are taken from RFC-3526. - * - * @see - * RFC-3526: More Modular Exponential (MODP) Diffie-Hellman groups for Internet Key Exchange (IKE) - */ -public enum ElGamalLength implements KeyLength { - - /** - * prime: 2^1536 - 2^1472 - 1 + 2^64 * { [2^1406 pi] + 741804 }. - * generator: 2 - */ - _1536(1536, "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA237327FFFFFFFFFFFFFFFF", "2"), - - /** - * prime: 2^2048 - 2^1984 - 1 + 2^64 * { [2^1918 pi] + 124476 }. - * generator: 2 - */ - _2048(2048, "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AACAA68FFFFFFFFFFFFFFFF", "2"), - - /** - * prime: 2^3072 - 2^3008 - 1 + 2^64 * { [2^2942 pi] + 1690314 }. - * generator: 2 - */ - _3072(3072, "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B18177B200CBBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFCE0FD108E4B82D120A93AD2CAFFFFFFFFFFFFFFFF", "2"), - - /** - * prime: 2^4096 - 2^4032 - 1 + 2^64 * { [2^3966 pi] + 240904 }. - * generator: 2 - */ - _4096(4096, "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B18177B200CBBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFCE0FD108E4B82D120A92108011A723C12A787E6D788719A10BDBA5B2699C327186AF4E23C1A946834B6150BDA2583E9CA2AD44CE8DBBBC2DB04DE8EF92E8EFC141FBECAA6287C59474E6BC05D99B2964FA090C3A2233BA186515BE7ED1F612970CEE2D7AFB81BDD762170481CD0069127D5B05AA993B4EA988D8FDDC186FFB7DC90A6C08F4DF435C934063199FFFFFFFFFFFFFFFF", "2"), - - /** - * prime: 2^6144 - 2^6080 - 1 + 2^64 * { [2^6014 pi] + 929484 }. - * generator: 2 - */ - _6144(6144, "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B18177B200CBBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFCE0FD108E4B82D120A92108011A723C12A787E6D788719A10BDBA5B2699C327186AF4E23C1A946834B6150BDA2583E9CA2AD44CE8DBBBC2DB04DE8EF92E8EFC141FBECAA6287C59474E6BC05D99B2964FA090C3A2233BA186515BE7ED1F612970CEE2D7AFB81BDD762170481CD0069127D5B05AA993B4EA988D8FDDC186FFB7DC90A6C08F4DF435C93402849236C3FAB4D27C7026C1D4DCB2602646DEC9751E763DBA37BDF8FF9406AD9E530EE5DB382F413001AEB06A53ED9027D831179727B0865A8918DA3EDBEBCF9B14ED44CE6CBACED4BB1BDB7F1447E6CC254B332051512BD7AF426FB8F401378CD2BF5983CA01C64B92ECF032EA15D1721D03F482D7CE6E74FEF6D55E702F46980C82B5A84031900B1C9E59E7C97FBEC7E8F323A97A7E36CC88BE0F1D45B7FF585AC54BD407B22B4154AACC8F6D7EBF48E1D814CC5ED20F8037E0A79715EEF29BE32806A1D58BB7C5DA76F550AA3D8A1FBFF0EB19CCB1A313D55CDA56C9EC2EF29632387FE8D76E3C0468043E8F663F4860EE12BF2D5B0B7474D6E694F91E6DCC4024FFFFFFFFFFFFFFFF", "2"), - - /** - * prime: 2^8192 - 2^8128 - 1 + 2^64 * { [2^8062 pi] + 4743158 }. - * generator: 2 - */ - _8192(8192, "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B18177B200CBBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFCE0FD108E4B82D120A92108011A723C12A787E6D788719A10BDBA5B2699C327186AF4E23C1A946834B6150BDA2583E9CA2AD44CE8DBBBC2DB04DE8EF92E8EFC141FBECAA6287C59474E6BC05D99B2964FA090C3A2233BA186515BE7ED1F612970CEE2D7AFB81BDD762170481CD0069127D5B05AA993B4EA988D8FDDC186FFB7DC90A6C08F4DF435C93402849236C3FAB4D27C7026C1D4DCB2602646DEC9751E763DBA37BDF8FF9406AD9E530EE5DB382F413001AEB06A53ED9027D831179727B0865A8918DA3EDBEBCF9B14ED44CE6CBACED4BB1BDB7F1447E6CC254B332051512BD7AF426FB8F401378CD2BF5983CA01C64B92ECF032EA15D1721D03F482D7CE6E74FEF6D55E702F46980C82B5A84031900B1C9E59E7C97FBEC7E8F323A97A7E36CC88BE0F1D45B7FF585AC54BD407B22B4154AACC8F6D7EBF48E1D814CC5ED20F8037E0A79715EEF29BE32806A1D58BB7C5DA76F550AA3D8A1FBFF0EB19CCB1A313D55CDA56C9EC2EF29632387FE8D76E3C0468043E8F663F4860EE12BF2D5B0B7474D6E694F91E6DBE115974A3926F12FEE5E438777CB6A932DF8CD8BEC4D073B931BA3BC832B68D9DD300741FA7BF8AFC47ED2576F6936BA424663AAB639C5AE4F5683423B4742BF1C978238F16CBE39D652DE3FDB8BEFC848AD922222E04A4037C0713EB57A81A23F0C73473FC646CEA306B4BCBC8862F8385DDFA9D4B7FA2C087E879683303ED5BDD3A062B3CF5B3A278A66D2A13F83F44F82DDF310EE074AB6A364597E899A0255DC164F31CC50846851DF9AB48195DED7EA1B1D510BD7EE74D73FAF36BC31ECFA268359046F4EB879F924009438B481C6CD7889A002ED5EE382BC9190DA6FC026E479558E4475677E9AA9E3050E2765694DFC81F56E880B96E7160C980DD98EDD3DFFFFFFFFFFFFFFFFF", "2") - ; - - private final int length; - private final BigInteger p; - private final BigInteger g; - - ElGamalLength(int length, String p, String g) { - this.length = length; - this.p = new BigInteger(p, 16); - this.g = new BigInteger(g, 16); - } - - @Override - public int getLength() { - return length; - } - - public BigInteger getP() { - return p; - } - - public BigInteger getG() { - return g; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/elgamal/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/elgamal/package-info.java deleted file mode 100644 index 19bc0214..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/elgamal/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Classes related to ElGamal. - */ -package org.pgpainless.key.generation.type.elgamal; diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/package-info.java deleted file mode 100644 index bf048484..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Classes describing different OpenPGP key types. - */ -package org.pgpainless.key.generation.type; diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/rsa/RSA.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/rsa/RSA.java deleted file mode 100644 index 231c95a2..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/rsa/RSA.java +++ /dev/null @@ -1,43 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.generation.type.rsa; - -import javax.annotation.Nonnull; -import java.security.spec.AlgorithmParameterSpec; -import java.security.spec.RSAKeyGenParameterSpec; - -import org.pgpainless.algorithm.PublicKeyAlgorithm; -import org.pgpainless.key.generation.type.KeyType; - -/** - * Key type that specifies the RSA_GENERAL algorithm. - */ -public class RSA implements KeyType { - - private final RsaLength length; - - RSA(@Nonnull RsaLength length) { - this.length = length; - } - - public static RSA withLength(@Nonnull RsaLength length) { - return new RSA(length); - } - - @Override - public String getName() { - return "RSA"; - } - - @Override - public PublicKeyAlgorithm getAlgorithm() { - return PublicKeyAlgorithm.RSA_GENERAL; - } - - @Override - public AlgorithmParameterSpec getAlgorithmSpec() { - return new RSAKeyGenParameterSpec(length.getLength(), RSAKeyGenParameterSpec.F4); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/rsa/RsaLength.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/rsa/RsaLength.java deleted file mode 100644 index 74951f0d..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/rsa/RsaLength.java +++ /dev/null @@ -1,29 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.generation.type.rsa; - -import org.pgpainless.key.generation.type.KeyLength; - -public enum RsaLength implements KeyLength { - @Deprecated - _1024(1024), - @Deprecated - _2048(2048), - _3072(3072), - _4096(4096), - _8192(8192), - ; - - private final int length; - - RsaLength(int length) { - this.length = length; - } - - @Override - public int getLength() { - return length; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/rsa/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/rsa/package-info.java deleted file mode 100644 index 2a2a0120..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/rsa/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Classes related to RSA. - */ -package org.pgpainless.key.generation.type.rsa; diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/xdh/XDH.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/xdh/XDH.java deleted file mode 100644 index db8d7d1e..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/xdh/XDH.java +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.generation.type.xdh; - -import java.security.spec.AlgorithmParameterSpec; - -import org.bouncycastle.jce.spec.ECNamedCurveGenParameterSpec; -import org.pgpainless.algorithm.PublicKeyAlgorithm; -import org.pgpainless.key.generation.type.KeyType; - -public final class XDH implements KeyType { - - private final XDHSpec spec; - - private XDH(XDHSpec spec) { - this.spec = spec; - } - - public static XDH fromSpec(XDHSpec spec) { - return new XDH(spec); - } - - @Override - public String getName() { - return "XDH"; - } - - @Override - public PublicKeyAlgorithm getAlgorithm() { - return PublicKeyAlgorithm.ECDH; - } - - @Override - public AlgorithmParameterSpec getAlgorithmSpec() { - return new ECNamedCurveGenParameterSpec(spec.getName()); - } - -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/xdh/XDHSpec.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/xdh/XDHSpec.java deleted file mode 100644 index e33fecd4..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/xdh/XDHSpec.java +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.generation.type.xdh; - -import javax.annotation.Nonnull; - -public enum XDHSpec { - _X25519("X25519", "curve25519"), - ; - - final String name; - final String curveName; - - XDHSpec(@Nonnull String name, @Nonnull String curveName) { - this.name = name; - this.curveName = curveName; - } - - public String getName() { - return name; - } - - public String getCurveName() { - return curveName; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/xdh/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/xdh/package-info.java deleted file mode 100644 index 96af405a..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/xdh/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Classes related to Diffie-Hellman on the X25519 curve. - */ -package org.pgpainless.key.generation.type.xdh; diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyAccessor.java b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyAccessor.java deleted file mode 100644 index b4f61090..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyAccessor.java +++ /dev/null @@ -1,125 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.info; - -import java.util.Set; - -import javax.annotation.Nonnull; - -import org.bouncycastle.openpgp.PGPSignature; -import org.pgpainless.algorithm.CompressionAlgorithm; -import org.pgpainless.algorithm.HashAlgorithm; -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; -import org.pgpainless.key.SubkeyIdentifier; -import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil; - -public abstract class KeyAccessor { - - protected final KeyRingInfo info; - protected final SubkeyIdentifier key; - - KeyAccessor(KeyRingInfo info, SubkeyIdentifier key) { - this.info = info; - this.key = key; - } - - /** - * Depending on the way we address the key (key-id or user-id), return the respective {@link PGPSignature} - * which contains the algorithm preferences we are going to use. - * - * If we address a key via its user-id, we want to rely on the algorithm preferences in the user-id certification, - * while we would instead rely on those in the direct-key signature if we'd address the key by key-id. - * - * @return signature - */ - public abstract @Nonnull PGPSignature getSignatureWithPreferences(); - - /** - * Return preferred symmetric key encryption algorithms. - * - * @return preferred symmetric algorithms - */ - public Set getPreferredSymmetricKeyAlgorithms() { - return SignatureSubpacketsUtil.parsePreferredSymmetricKeyAlgorithms(getSignatureWithPreferences()); - } - - /** - * Return preferred hash algorithms. - * - * @return preferred hash algorithms - */ - public Set getPreferredHashAlgorithms() { - return SignatureSubpacketsUtil.parsePreferredHashAlgorithms(getSignatureWithPreferences()); - } - - /** - * Return preferred compression algorithms. - * - * @return preferred compression algorithms - */ - public Set getPreferredCompressionAlgorithms() { - return SignatureSubpacketsUtil.parsePreferredCompressionAlgorithms(getSignatureWithPreferences()); - } - - /** - * Address the key via a user-id (eg "Alice <alice@wonderland.lit>). - * In this case we are sourcing preferred algorithms from the user-id certification first. - */ - public static class ViaUserId extends KeyAccessor { - - private final String userId; - - /** - * Access a key via user-id. - * - * @param info info about a key at a given date - * @param key id of the subkey - * @param userId user-id - */ - public ViaUserId(KeyRingInfo info, SubkeyIdentifier key, String userId) { - super(info, key); - this.userId = userId; - } - - @Override - public @Nonnull PGPSignature getSignatureWithPreferences() { - PGPSignature signature = info.getLatestUserIdCertification(userId); - if (signature != null) { - return signature; - } - throw new IllegalStateException("No valid user-id certification signature found for '" + userId + "'."); - } - } - - /** - * Address the key via key-id. - * In this case we are sourcing preferred algorithms from the keys direct-key signature first. - */ - public static class ViaKeyId extends KeyAccessor { - - /** - * Address the key via key-id. - * @param info info about the key at a given date - * @param key key-id - */ - public ViaKeyId(KeyRingInfo info, SubkeyIdentifier key) { - super(info, key); - } - - @Override - public @Nonnull PGPSignature getSignatureWithPreferences() { - PGPSignature signature = info.getLatestDirectKeySelfSignature(); - if (signature != null) { - return signature; - } - - signature = info.getLatestUserIdCertification(info.getPrimaryUserId()); - if (signature == null) { - throw new IllegalStateException("No valid signature found."); - } - return signature; - } - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyInfo.java b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyInfo.java deleted file mode 100644 index e34de8c9..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyInfo.java +++ /dev/null @@ -1,129 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub , 2021 Flowcrypt a.s. -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.info; - -import org.bouncycastle.asn1.ASN1ObjectIdentifier; -import org.bouncycastle.bcpg.ECDHPublicBCPGKey; -import org.bouncycastle.bcpg.ECDSAPublicBCPGKey; -import org.bouncycastle.bcpg.ECPublicBCPGKey; -import org.bouncycastle.bcpg.EdDSAPublicBCPGKey; -import org.bouncycastle.bcpg.S2K; -import org.bouncycastle.jcajce.provider.asymmetric.util.ECUtil; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPSecretKey; -import org.pgpainless.algorithm.PublicKeyAlgorithm; -import org.pgpainless.key.generation.type.eddsa.EdDSACurve; - -public class KeyInfo { - - private final PGPSecretKey secretKey; - private final PGPPublicKey publicKey; - - public KeyInfo(PGPSecretKey secretKey) { - this.secretKey = secretKey; - this.publicKey = secretKey.getPublicKey(); - } - - public KeyInfo(PGPPublicKey publicKey) { - this.publicKey = publicKey; - this.secretKey = null; - } - - public String getCurveName() { - return getCurveName(publicKey); - } - - /** - * Returns indication that a contained secret key is encrypted. - * - * @return true if secret key is encrypted, false if secret key is not encrypted or there is public key only. - */ - public boolean isEncrypted() { - return secretKey != null && isEncrypted(secretKey); - } - - /** - * Returns indication that a contained secret key is not encrypted. - * - * @return true if secret key is not encrypted or there is public key only, false if secret key is encrypted. - */ - public boolean isDecrypted() { - return secretKey == null || isDecrypted(secretKey); - } - - /** - * Returns indication that a contained secret key has S2K of a type GNU_DUMMY_S2K. - * - * @return true if secret key has S2K of a type GNU_DUMMY_S2K, false if there is public key only, - * or S2K on the secret key is absent or not of a type GNU_DUMMY_S2K. - */ - public boolean hasDummyS2K() { - return secretKey != null && hasDummyS2K(secretKey); - } - - public static String getCurveName(PGPPublicKey publicKey) { - PublicKeyAlgorithm algorithm = PublicKeyAlgorithm.fromId(publicKey.getAlgorithm()); - ECPublicBCPGKey key; - switch (algorithm) { - case ECDSA: { - key = (ECDSAPublicBCPGKey) publicKey.getPublicKeyPacket().getKey(); - break; - } - case ECDH: { - key = (ECDHPublicBCPGKey) publicKey.getPublicKeyPacket().getKey(); - break; - } - case EDDSA: { - key = (EdDSAPublicBCPGKey) publicKey.getPublicKeyPacket().getKey(); - break; - } - default: - throw new IllegalArgumentException("Not an elliptic curve public key (" + algorithm + ")"); - } - return getCurveName(key); - } - - public static String getCurveName(ECPublicBCPGKey key) { - ASN1ObjectIdentifier identifier = key.getCurveOID(); - - // Workaround for ECUtil not recognizing ed25519 - if (identifier.getId().equals("1.3.6.1.4.1.11591.15.1")) { - return EdDSACurve._Ed25519.getName(); - } - - return ECUtil.getCurveName(identifier); - } - - /** - * Returns indication that a secret key is encrypted. - * - * @param secretKey A secret key to examine. - * @return true if secret key is encrypted, false otherwise. - */ - public static boolean isEncrypted(PGPSecretKey secretKey) { - return secretKey.getS2KUsage() != 0; - } - - /** - * Returns indication that a secret key is not encrypted. - * - * @param secretKey A secret key to examine. - * @return true if secret key is encrypted, false otherwise. - */ - public static boolean isDecrypted(PGPSecretKey secretKey) { - return secretKey.getS2KUsage() == 0; - } - - /** - * Returns indication that a secret key has S2K of a type GNU_DUMMY_S2K. - * - * @param secretKey A secret key to examine. - * @return true if secret key has S2K of a type GNU_DUMMY_S2K, false otherwise. - */ - public static boolean hasDummyS2K(PGPSecretKey secretKey) { - final S2K s2k = secretKey.getS2K(); - return s2k != null && s2k.getType() == S2K.GNU_DUMMY_S2K; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java deleted file mode 100644 index f5a23ca0..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java +++ /dev/null @@ -1,943 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub , 2021 Flowcrypt a.s. -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.info; - -import static org.pgpainless.util.CollectionUtils.iteratorToList; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.NoSuchElementException; -import java.util.Set; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import org.bouncycastle.bcpg.sig.PrimaryUserID; -import org.bouncycastle.openpgp.PGPKeyRing; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.PGPSignature; -import org.pgpainless.PGPainless; -import org.pgpainless.algorithm.CompressionAlgorithm; -import org.pgpainless.algorithm.EncryptionPurpose; -import org.pgpainless.algorithm.HashAlgorithm; -import org.pgpainless.algorithm.KeyFlag; -import org.pgpainless.algorithm.PublicKeyAlgorithm; -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; -import org.pgpainless.exception.KeyValidationError; -import org.pgpainless.key.OpenPgpV4Fingerprint; -import org.pgpainless.key.SubkeyIdentifier; -import org.pgpainless.policy.Policy; -import org.pgpainless.signature.SignaturePicker; -import org.pgpainless.signature.SignatureUtils; -import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil; - -/** - * Utility class to quickly extract certain information from a {@link PGPPublicKeyRing}/{@link PGPSecretKeyRing}. - */ -public class KeyRingInfo { - - private static final Pattern PATTERN_EMAIL = Pattern.compile("[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,6}"); - - private final PGPKeyRing keys; - private Signatures signatures; - - /** - * Evaluate the key ring at creation time of the given signature. - * - * @param keyRing key ring - * @param signature signature - * @return info of key ring at signature creation time - */ - public static KeyRingInfo evaluateForSignature(PGPKeyRing keyRing, PGPSignature signature) { - return new KeyRingInfo(keyRing, signature.getCreationTime()); - } - - /** - * Evaluate the key ring right now. - * - * @param keys key ring - */ - public KeyRingInfo(PGPKeyRing keys) { - this(keys, new Date()); - } - - /** - * Evaluate the key ring at the provided validation date. - * - * @param keys key ring - * @param validationDate date of validation - */ - public KeyRingInfo(PGPKeyRing keys, Date validationDate) { - this.keys = keys; - this.signatures = new Signatures(keys, validationDate, PGPainless.getPolicy()); - } - - /** - * Return the first {@link PGPPublicKey} of this key ring. - * - * @return public key - */ - public PGPPublicKey getPublicKey() { - return keys.getPublicKey(); - } - - /** - * Return the public key with the given fingerprint. - * - * @param fingerprint fingerprint - * @return public key or null - */ - public @Nullable PGPPublicKey getPublicKey(OpenPgpV4Fingerprint fingerprint) { - return getPublicKey(fingerprint.getKeyId()); - } - - /** - * Return the public key with the given key id. - * - * @param keyId key id - * @return public key or null - */ - public @Nullable PGPPublicKey getPublicKey(long keyId) { - return getPublicKey(keys, keyId); - } - - /** - * Return the public key with the given key id from the provided key ring. - * - * @param keyRing key ring - * @param keyId key id - * @return public key or null - */ - public static @Nullable PGPPublicKey getPublicKey(PGPKeyRing keyRing, long keyId) { - return keyRing.getPublicKey(keyId); - } - - /** - * Return true if the public key with the given key id is bound to the key ring properly. - * - * @param keyId key id - * @return true if key is bound validly - */ - public boolean isKeyValidlyBound(long keyId) { - PGPPublicKey publicKey = keys.getPublicKey(keyId); - if (publicKey == null) { - return false; - } - - if (publicKey == getPublicKey()) { - if (signatures.primaryKeyRevocation != null && SignatureUtils.isHardRevocation(signatures.primaryKeyRevocation)) { - return false; - } - return signatures.primaryKeyRevocation == null; - } - - PGPSignature binding = signatures.subkeyBindings.get(keyId); - PGPSignature revocation = signatures.subkeyRevocations.get(keyId); - - // No valid binding - if (binding == null || SignatureUtils.isSignatureExpired(binding)) { - return false; - } - - // Revocation - if (revocation != null) { - if (SignatureUtils.isHardRevocation(revocation)) { - // Subkey is hard revoked - return false; - } else { - if (!SignatureUtils.isSignatureExpired(revocation) - && revocation.getCreationTime().after(binding.getCreationTime())) { - // Key is soft-revoked, not yet re-bound - return false; - } - } - } - - return true; - } - - /** - * Return all {@link PGPPublicKey PGPPublicKeys} of this key ring. - * The first key in the list being the primary key. - * Note that the list is unmodifiable. - * - * @return list of public keys - */ - public List getPublicKeys() { - Iterator iterator = keys.getPublicKeys(); - List list = iteratorToList(iterator); - return Collections.unmodifiableList(list); - } - - /** - * Return the primary {@link PGPSecretKey} of this key ring or null if the key ring is not a {@link PGPSecretKeyRing}. - * - * @return primary secret key or null if the key ring is public - */ - public @Nullable PGPSecretKey getSecretKey() { - if (keys instanceof PGPSecretKeyRing) { - PGPSecretKeyRing secretKeys = (PGPSecretKeyRing) keys; - return secretKeys.getSecretKey(); - } - return null; - } - - /** - * Return the secret key with the given fingerprint. - * - * @param fingerprint fingerprint - * @return secret key or null - */ - public @Nullable PGPSecretKey getSecretKey(OpenPgpV4Fingerprint fingerprint) { - return getSecretKey(fingerprint.getKeyId()); - } - - /** - * Return the secret key with the given key id. - * - * @param keyId key id - * @return secret key or null - */ - public @Nullable PGPSecretKey getSecretKey(long keyId) { - if (keys instanceof PGPSecretKeyRing) { - return ((PGPSecretKeyRing) keys).getSecretKey(keyId); - } - return null; - } - - /** - * Return all secret keys of the key ring. - * If the key ring is a {@link PGPPublicKeyRing}, then return an empty list. - * Note that the list is unmodifiable. - * - * @return list of secret keys - */ - public List getSecretKeys() { - if (keys instanceof PGPSecretKeyRing) { - PGPSecretKeyRing secretKeys = (PGPSecretKeyRing) keys; - Iterator iterator = secretKeys.getSecretKeys(); - return Collections.unmodifiableList(iteratorToList(iterator)); - } - return Collections.emptyList(); - } - - /** - * Return the key id of the primary key of this key ring. - * - * @return key id - */ - public long getKeyId() { - return getPublicKey().getKeyID(); - } - - /** - * Return the {@link OpenPgpV4Fingerprint} of this key ring. - * - * @return fingerprint - */ - public OpenPgpV4Fingerprint getFingerprint() { - return new OpenPgpV4Fingerprint(getPublicKey()); - } - - /** - * Return the primary user-id of the key ring. - * - * Note: If no user-id is marked as primary key using a {@link PrimaryUserID} packet, this method returns the - * first valid user-id, otherwise null. - * - * @return primary user-id or null - */ - public @Nullable String getPrimaryUserId() { - String primaryUserId = null; - Date modificationDate = null; - - List validUserIds = getValidUserIds(); - if (validUserIds.isEmpty()) { - return null; - } - - for (String userId : validUserIds) { - - PGPSignature signature = signatures.userIdCertifications.get(userId); - if (signature == null) { - continue; - } - - PrimaryUserID subpacket = SignatureSubpacketsUtil.getPrimaryUserId(signature); - if (subpacket != null && subpacket.isPrimaryUserID()) { - // if there are multiple primary userIDs, return most recently signed - if (modificationDate == null || modificationDate.before(signature.getCreationTime())) { - primaryUserId = userId; - modificationDate = signature.getCreationTime(); - } - } - } - // Workaround for keys with only one user-id but no primary user-id packet. - if (primaryUserId == null) { - return validUserIds.get(0); - } - - return primaryUserId; - } - - /** - * Return a list of all user-ids of the primary key. - * Note: This list might also contain expired / revoked user-ids. - * Consider using {@link #getValidUserIds()} instead. - * - * @return list of user-ids - */ - public List getUserIds() { - Iterator iterator = getPublicKey().getUserIDs(); - List userIds = iteratorToList(iterator); - return userIds; - } - - /** - * Return a list of valid user-ids. - * - * @return valid user-ids - */ - public List getValidUserIds() { - List valid = new ArrayList<>(); - List userIds = getUserIds(); - for (String userId : userIds) { - if (isUserIdValid(userId)) { - valid.add(userId); - } - } - return valid; - } - - /** - * Return true if the provided user-id is valid. - * - * @param userId user-id - * @return true if user-id is valid - */ - public boolean isUserIdValid(String userId) { - PGPSignature certification = signatures.userIdCertifications.get(userId); - PGPSignature revocation = signatures.userIdRevocations.get(userId); - - // If user-id is expired, certification will be null. - if (certification == null) { - return false; - } - if (SignatureUtils.isSignatureExpired(certification)) { - return false; - } - // Not revoked -> valid - if (revocation == null) { - return true; - } - // Hard revocation -> invalid - if (SignatureUtils.isHardRevocation(revocation)) { - return false; - } - // Soft revocation -> valid if certification is newer than revocation (revalidation) - return certification.getCreationTime().after(revocation.getCreationTime()); - } - - /** - * Return a list of all user-ids of the primary key that appear to be email-addresses. - * Note: This list might contain expired / revoked user-ids. - * - * @return email addresses - */ - public List getEmailAddresses() { - List userIds = getUserIds(); - List emails = new ArrayList<>(); - for (String userId : userIds) { - Matcher matcher = PATTERN_EMAIL.matcher(userId); - if (matcher.find()) { - emails.add(matcher.group()); - } - } - return emails; - } - - /** - * Return the latest direct-key self signature. - * - * Note: This signature might be expired (check with {@link SignatureUtils#isSignatureExpired(PGPSignature)}). - * - * @return latest direct key self-signature or null - */ - public @Nullable PGPSignature getLatestDirectKeySelfSignature() { - return signatures.primaryKeySelfSignature; - } - - /** - * Return the latest revocation self-signature on the primary key. - * - * @return revocation or null - */ - public @Nullable PGPSignature getRevocationSelfSignature() { - return signatures.primaryKeyRevocation; - } - - /** - * Return the latest certification self-signature on the provided user-id. - * - * @param userId user-id - * @return certification signature or null - */ - public @Nullable PGPSignature getLatestUserIdCertification(String userId) { - return signatures.userIdCertifications.get(userId); - } - - /** - * Return the latest user-id revocation signature for the provided user-id. - * - * @param userId user-id - * @return revocation or null - */ - public @Nullable PGPSignature getUserIdRevocation(String userId) { - return signatures.userIdRevocations.get(userId); - } - - /** - * Return the currently active subkey binding signature for the subkey with the provided key-id. - * - * @param keyId subkey id - * @return subkey binding signature or null - */ - public @Nullable PGPSignature getCurrentSubkeyBindingSignature(long keyId) { - return signatures.subkeyBindings.get(keyId); - } - - /** - * Return the latest subkey binding revocation signature for the subkey with the given key-id. - * - * @param keyId subkey id - * @return subkey binding revocation or null - */ - public @Nullable PGPSignature getSubkeyRevocationSignature(long keyId) { - return signatures.subkeyRevocations.get(keyId); - } - - /** - * Return the a list of {@link KeyFlag KeyFlags} that apply to the subkey with the provided key id. - * @param keyId key-id - * @return list of key flags - */ - public @Nonnull List getKeyFlagsOf(long keyId) { - // key is primary key - if (getPublicKey().getKeyID() == keyId) { - - PGPSignature directKeySignature = getLatestDirectKeySelfSignature(); - if (directKeySignature != null) { - List keyFlags = SignatureSubpacketsUtil.parseKeyFlags(directKeySignature); - if (keyFlags != null) { - return keyFlags; - } - } - - String primaryUserId = getPrimaryUserId(); - if (primaryUserId != null) { - PGPSignature userIdSignature = getLatestUserIdCertification(primaryUserId); - List keyFlags = SignatureSubpacketsUtil.parseKeyFlags(userIdSignature); - if (keyFlags != null) { - return keyFlags; - } - } - } - // Key is subkey - else { - PGPSignature bindingSignature = getCurrentSubkeyBindingSignature(keyId); - if (bindingSignature != null) { - List keyFlags = SignatureSubpacketsUtil.parseKeyFlags(bindingSignature); - if (keyFlags != null) { - return keyFlags; - } - } - } - return Collections.emptyList(); - } - - /** - * Return a list of {@link KeyFlag KeyFlags} that apply to the given user-id. - * - * @param userId user-id - * @return key flags - */ - public @Nonnull List getKeyFlagsOf(String userId) { - if (!isUserIdValid(userId)) { - return Collections.emptyList(); - } - - PGPSignature userIdCertification = getLatestUserIdCertification(userId); - if (userIdCertification == null) { - throw new AssertionError("While user-id '" + userId + "' was reported as valid, there appears to be no certification for it."); - } - - List keyFlags = SignatureSubpacketsUtil.parseKeyFlags(userIdCertification); - if (keyFlags != null) { - return keyFlags; - } - return Collections.emptyList(); - } - - /** - * Return the algorithm of the primary key. - * - * @return public key algorithm - */ - public PublicKeyAlgorithm getAlgorithm() { - return PublicKeyAlgorithm.fromId(getPublicKey().getAlgorithm()); - } - - /** - * Return the creation date of the primary key. - * - * @return creation date - */ - public Date getCreationDate() { - return getPublicKey().getCreationTime(); - } - - /** - * Return the date on which the key ring was last modified. - * This date corresponds to the date of the last signature that was made on this key ring by the primary key. - * - * @return last modification date. - */ - public @Nullable Date getLastModified() { - PGPSignature mostRecent = getMostRecentSignature(); - if (mostRecent == null) { - // No sigs found. Return public key creation date instead. - return getLatestKeyCreationDate(); - } - return mostRecent.getCreationTime(); - } - - /** - * Return the creation time of the latest added subkey. - * - * @return latest key creation time - */ - public @Nonnull Date getLatestKeyCreationDate() { - Date latestCreation = null; - for (PGPPublicKey key : getPublicKeys()) { - if (!isKeyValidlyBound(key.getKeyID())) { - continue; - } - Date keyCreation = key.getCreationTime(); - if (latestCreation == null || latestCreation.before(keyCreation)) { - latestCreation = keyCreation; - } - } - if (latestCreation == null) { - throw new AssertionError("Apparently there is no validly bound key in this key ring."); - } - return latestCreation; - } - - private @Nullable PGPSignature getMostRecentSignature() { - Set allSignatures = new HashSet<>(); - PGPSignature mostRecentSelfSignature = getLatestDirectKeySelfSignature(); - PGPSignature revocationSelfSignature = getRevocationSelfSignature(); - if (mostRecentSelfSignature != null) allSignatures.add(mostRecentSelfSignature); - if (revocationSelfSignature != null) allSignatures.add(revocationSelfSignature); - allSignatures.addAll(signatures.userIdCertifications.values()); - allSignatures.addAll(signatures.userIdRevocations.values()); - allSignatures.addAll(signatures.subkeyBindings.values()); - allSignatures.addAll(signatures.subkeyRevocations.values()); - - PGPSignature mostRecent = null; - for (PGPSignature signature : allSignatures) { - if (mostRecent == null || signature.getCreationTime().after(mostRecent.getCreationTime())) { - mostRecent = signature; - } - } - return mostRecent; - } - - /** - * Return the date on which the primary key was revoked, or null if it has not yet been revoked. - * - * @return revocation date or null - */ - public @Nullable Date getRevocationDate() { - return getRevocationSelfSignature() == null ? null : getRevocationSelfSignature().getCreationTime(); - } - - /** - * Return the date of expiration of the primary key or null if the key has no expiration date. - * - * @return expiration date - */ - public @Nullable Date getPrimaryKeyExpirationDate() { - PGPSignature directKeySig = getLatestDirectKeySelfSignature(); - if (directKeySig != null) { - Date directKeyExpirationDate = SignatureSubpacketsUtil.getKeyExpirationTimeAsDate(directKeySig, getPublicKey()); - if (directKeyExpirationDate != null) { - return directKeyExpirationDate; - } - } - - PGPSignature primaryUserIdCertification = getLatestUserIdCertification(getPrimaryUserId()); - if (primaryUserIdCertification != null) { - return SignatureSubpacketsUtil.getKeyExpirationTimeAsDate(primaryUserIdCertification, getPublicKey()); - } - - throw new NoSuchElementException("No suitable signatures found on the key."); - } - - /** - * Return the expiration date of the subkey with the provided fingerprint. - * - * @param fingerprint subkey fingerprint - * @return expiration date or null - */ - public @Nullable Date getSubkeyExpirationDate(OpenPgpV4Fingerprint fingerprint) { - if (getPublicKey().getKeyID() == fingerprint.getKeyId()) { - return getPrimaryKeyExpirationDate(); - } - - PGPPublicKey subkey = getPublicKey(fingerprint.getKeyId()); - if (subkey == null) { - throw new NoSuchElementException("No subkey with fingerprint " + fingerprint + " found."); - } - - PGPSignature bindingSig = getCurrentSubkeyBindingSignature(fingerprint.getKeyId()); - if (bindingSig == null) { - throw new AssertionError("Subkey has no valid binding signature."); - } - - return SignatureUtils.getKeyExpirationDate(subkey.getCreationTime(), bindingSig); - } - - /** - * Return the latest date on which the key ring is still usable for the given key flag. - * If a only a subkey is carrying the required flag and the primary key expires earlier than the subkey, - * the expiry date of the primary key is returned. - * - * This method might return null, if the primary key and a subkey with the required flag does not expire. - * @param use key flag representing the use case, eg. {@link KeyFlag#SIGN_DATA} or - * {@link KeyFlag#ENCRYPT_COMMS}/{@link KeyFlag#ENCRYPT_STORAGE}. - * @return latest date on which the key ring can be used for the given use case, or null if it can be used indefinitely. - */ - public Date getExpirationDateForUse(KeyFlag use) { - if (use == KeyFlag.SPLIT || use == KeyFlag.SHARED) { - throw new IllegalArgumentException("SPLIT and SHARED are not uses, but properties."); - } - - Date primaryExpiration = getPrimaryKeyExpirationDate(); - List nonExpiringSubkeys = new ArrayList<>(); - Date latestSubkeyExpirationDate = null; - - List keysWithFlag = getKeysWithKeyFlag(use); - if (keysWithFlag.isEmpty()) { - throw new NoSuchElementException("No key with the required key flag found."); - } - - for (PGPPublicKey key : keysWithFlag) { - Date subkeyExpirationDate = getSubkeyExpirationDate(new OpenPgpV4Fingerprint(key)); - if (subkeyExpirationDate == null) { - nonExpiringSubkeys.add(key); - } else { - if (latestSubkeyExpirationDate == null || subkeyExpirationDate.after(latestSubkeyExpirationDate)) { - latestSubkeyExpirationDate = subkeyExpirationDate; - } - } - } - - if (nonExpiringSubkeys.isEmpty()) { - if (latestSubkeyExpirationDate != null) { - if (primaryExpiration == null) { - return latestSubkeyExpirationDate; - } - if (latestSubkeyExpirationDate.before(primaryExpiration)) { - return latestSubkeyExpirationDate; - } - } - } - return primaryExpiration; - } - - /** - * Return true if the key ring is a {@link PGPSecretKeyRing}. - * If it is a {@link PGPPublicKeyRing} return false and if it is neither, throw an {@link AssertionError}. - * - * @return true if the key ring is a secret key ring. - */ - public boolean isSecretKey() { - if (keys instanceof PGPSecretKeyRing) { - return true; - } else if (keys instanceof PGPPublicKeyRing) { - return false; - } else { - throw new AssertionError("Expected PGPKeyRing to be either PGPPublicKeyRing or PGPSecretKeyRing, but got " + keys.getClass().getName() + " instead."); - } - } - - /** - * Returns true when every secret key on the key ring is not encrypted. - * If there is at least one encrypted secret key on the key ring, returns false. - * If the key ring is a {@link PGPPublicKeyRing}, returns true. - * Sub-keys with S2K of a type GNU_DUMMY_S2K do not affect the result. - * - * @return true if all secret keys are unencrypted. - */ - public boolean isFullyDecrypted() { - if (!isSecretKey()) { - return true; - } - for (PGPSecretKey secretKey : getSecretKeys()) { - if (!KeyInfo.hasDummyS2K(secretKey) && KeyInfo.isEncrypted(secretKey)) { - return false; - } - } - return true; - } - - /** - * Returns true when every secret key on the key ring is encrypted. - * If there is at least one not encrypted secret key on the key ring, returns false. - * If the key ring is a {@link PGPPublicKeyRing}, returns false. - * Sub-keys with S2K of a type GNU_DUMMY_S2K do not affect a result. - * - * @return true if all secret keys are encrypted. - */ - public boolean isFullyEncrypted() { - if (!isSecretKey()) { - return false; - } - for (PGPSecretKey secretKey : getSecretKeys()) { - if (!KeyInfo.hasDummyS2K(secretKey) && KeyInfo.isDecrypted(secretKey)) { - return false; - } - } - return true; - } - - /** - * Return the version number of the public keys format. - * - * @return version - */ - public int getVersion() { - return keys.getPublicKey().getVersion(); - } - - /** - * Return a list of all subkeys which can be used for encryption of the given purpose. - * This list does not include expired or revoked keys. - * - * @param purpose purpose (encrypt data at rest / communications) - * @return encryption subkeys - */ - public @Nonnull List getEncryptionSubkeys(EncryptionPurpose purpose) { - Date primaryExpiration = getPrimaryKeyExpirationDate(); - if (primaryExpiration != null && primaryExpiration.before(new Date())) { - return Collections.emptyList(); - } - - Iterator subkeys = keys.getPublicKeys(); - List encryptionKeys = new ArrayList<>(); - while (subkeys.hasNext()) { - PGPPublicKey subKey = subkeys.next(); - - if (!isKeyValidlyBound(subKey.getKeyID())) { - continue; - } - - Date subkeyExpiration = getSubkeyExpirationDate(new OpenPgpV4Fingerprint(subKey)); - if (subkeyExpiration != null && subkeyExpiration.before(new Date())) { - continue; - } - - if (!subKey.isEncryptionKey()) { - continue; - } - - List keyFlags = getKeyFlagsOf(subKey.getKeyID()); - switch (purpose) { - case COMMUNICATIONS: - if (keyFlags.contains(KeyFlag.ENCRYPT_COMMS)) { - encryptionKeys.add(subKey); - } - break; - case STORAGE: - if (keyFlags.contains(KeyFlag.ENCRYPT_STORAGE)) { - encryptionKeys.add(subKey); - } - break; - case STORAGE_AND_COMMUNICATIONS: - if (keyFlags.contains(KeyFlag.ENCRYPT_COMMS) || keyFlags.contains(KeyFlag.ENCRYPT_STORAGE)) { - encryptionKeys.add(subKey); - } - break; - } - } - return encryptionKeys; - } - - /** - * Return a list of all keys which carry the provided key flag in their signature. - * - * @param flag flag - * @return keys with flag - */ - public List getKeysWithKeyFlag(KeyFlag flag) { - List keysWithFlag = new ArrayList<>(); - for (PGPPublicKey key : getPublicKeys()) { - List keyFlags = getKeyFlagsOf(key.getKeyID()); - if (keyFlags.contains(flag)) { - keysWithFlag.add(key); - } - } - - return keysWithFlag; - } - - /** - * Return a list of all subkeys that can be used for encryption with the given user-id. - * This list does not include expired or revoked keys. - * TODO: Does it make sense to pass in a user-id? - * Aren't the encryption subkeys the same, regardless of which user-id is used? - * - * @param userId user-id - * @param purpose encryption purpose - * @return encryption subkeys - */ - public @Nonnull List getEncryptionSubkeys(String userId, EncryptionPurpose purpose) { - if (userId != null && !isUserIdValid(userId)) { - throw new KeyValidationError(userId, getLatestUserIdCertification(userId), getUserIdRevocation(userId)); - } - - return getEncryptionSubkeys(purpose); - } - - /** - * Return a list of all subkeys which can be used to sign data. - * - * @return signing keys - */ - public @Nonnull List getSigningSubkeys() { - Iterator subkeys = keys.getPublicKeys(); - List signingKeys = new ArrayList<>(); - while (subkeys.hasNext()) { - PGPPublicKey subKey = subkeys.next(); - - if (!isKeyValidlyBound(subKey.getKeyID())) { - continue; - } - - List keyFlags = getKeyFlagsOf(subKey.getKeyID()); - if (keyFlags.contains(KeyFlag.SIGN_DATA)) { - signingKeys.add(subKey); - } - } - return signingKeys; - } - - /** - * Return the (sorted) set of preferred hash algorithms of the given key. - * - * @param userId user-id. If this is non-null, the hash algorithms are being extracted from the user-id - * certification signature first. - * @param keyID if of the key in question - * @return hash algorithm preferences - */ - public Set getPreferredHashAlgorithms(@Nullable String userId, long keyID) { - KeyAccessor keyAccessor = getKeyAccessor(userId, keyID); - return keyAccessor.getPreferredHashAlgorithms(); - } - - /** - * Return the (sorted) set of preferred symmetric encryption algorithms of the given key. - * - * @param userId user-id. If this is non-null, the symmetric encryption algorithms are being - * extracted from the user-id certification signature first. - * @param keyId if of the key in question - * @return symmetric encryption algorithm preferences - */ - public Set getPreferredSymmetricKeyAlgorithms(@Nullable String userId, long keyId) { - KeyAccessor keyAccessor = getKeyAccessor(userId, keyId); - return keyAccessor.getPreferredSymmetricKeyAlgorithms(); - } - - /** - * Return the (sorted) set of preferred compression algorithms of the given key. - * - * @param userId user-id. If this is non-null, the compression algorithms are being extracted from the user-id - * certification signature first. - * @param keyId if of the key in question - * @return compression algorithm preferences - */ - public Set getPreferredCompressionAlgorithms(@Nullable String userId, long keyId) { - KeyAccessor keyAccessor = getKeyAccessor(userId, keyId); - return keyAccessor.getPreferredCompressionAlgorithms(); - } - - private KeyAccessor getKeyAccessor(@Nullable String userId, long keyID) { - if (getPublicKey(keyID) == null) { - throw new IllegalArgumentException("No subkey with key id " + Long.toHexString(keyID) + " found on this key."); - } - if (userId != null && !getUserIds().contains(userId)) { - throw new IllegalArgumentException("No user-id '" + userId + "' found on this key."); - } - return userId == null ? new KeyAccessor.ViaKeyId(this, new SubkeyIdentifier(keys, keyID)) - : new KeyAccessor.ViaUserId(this, new SubkeyIdentifier(keys, keyID), userId); - } - - public static class Signatures { - - private final PGPSignature primaryKeyRevocation; - private final PGPSignature primaryKeySelfSignature; - private final Map userIdRevocations; - private final Map userIdCertifications; - private final Map subkeyRevocations; - private final Map subkeyBindings; - - public Signatures(PGPKeyRing keyRing, Date evaluationDate, Policy policy) { - primaryKeyRevocation = SignaturePicker.pickCurrentRevocationSelfSignature(keyRing, policy, evaluationDate); - primaryKeySelfSignature = SignaturePicker.pickLatestDirectKeySignature(keyRing, policy, evaluationDate); - userIdRevocations = new HashMap<>(); - userIdCertifications = new HashMap<>(); - subkeyRevocations = new HashMap<>(); - subkeyBindings = new HashMap<>(); - - for (Iterator it = keyRing.getPublicKey().getUserIDs(); it.hasNext(); ) { - String userId = it.next(); - PGPSignature revocation = SignaturePicker.pickCurrentUserIdRevocationSignature(keyRing, userId, policy, evaluationDate); - if (revocation != null) { - userIdRevocations.put(userId, revocation); - } - PGPSignature certification = SignaturePicker.pickLatestUserIdCertificationSignature(keyRing, userId, policy, evaluationDate); - if (certification != null) { - userIdCertifications.put(userId, certification); - } - } - - Iterator keys = keyRing.getPublicKeys(); - keys.next(); // Skip primary key - while (keys.hasNext()) { - PGPPublicKey subkey = keys.next(); - PGPSignature subkeyRevocation = SignaturePicker.pickCurrentSubkeyBindingRevocationSignature(keyRing, subkey, policy, evaluationDate); - if (subkeyRevocation != null) { - subkeyRevocations.put(subkey.getKeyID(), subkeyRevocation); - } - PGPSignature subkeyBinding = SignaturePicker.pickLatestSubkeyBindingSignature(keyRing, subkey, policy, evaluationDate); - if (subkeyBinding != null) { - subkeyBindings.put(subkey.getKeyID(), subkeyBinding); - } - } - } - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/info/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/key/info/package-info.java deleted file mode 100644 index 9f33dd40..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/info/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Extract information from PGPKeyRings. - */ -package org.pgpainless.key.info; diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/modification/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/key/modification/package-info.java deleted file mode 100644 index 9fa73e88..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/modification/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Classes that deal with modifications made to OpenPGP keys. - */ -package org.pgpainless.key.modification; diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java b/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java deleted file mode 100644 index 2785202c..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java +++ /dev/null @@ -1,637 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.modification.secretkeyring; - -import static org.pgpainless.util.CollectionUtils.iteratorToList; - -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Date; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.NoSuchElementException; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import org.bouncycastle.bcpg.S2K; -import org.bouncycastle.bcpg.SecretKeyPacket; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPKeyPair; -import org.bouncycastle.openpgp.PGPKeyRingGenerator; -import org.bouncycastle.openpgp.PGPPrivateKey; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.PGPSignatureGenerator; -import org.bouncycastle.openpgp.PGPSignatureSubpacketGenerator; -import org.bouncycastle.openpgp.PGPSignatureSubpacketVector; -import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; -import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; -import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder; -import org.bouncycastle.openpgp.operator.PGPDigestCalculator; -import org.pgpainless.algorithm.HashAlgorithm; -import org.pgpainless.algorithm.SignatureType; -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; -import org.pgpainless.implementation.ImplementationFactory; -import org.pgpainless.key.OpenPgpV4Fingerprint; -import org.pgpainless.key.generation.KeyRingBuilder; -import org.pgpainless.key.generation.KeySpec; -import org.pgpainless.key.protection.CachingSecretKeyRingProtector; -import org.pgpainless.key.protection.KeyRingProtectionSettings; -import org.pgpainless.key.protection.PasswordBasedSecretKeyRingProtector; -import org.pgpainless.key.protection.SecretKeyRingProtector; -import org.pgpainless.key.protection.UnlockSecretKey; -import org.pgpainless.key.protection.UnprotectedKeysProtector; -import org.pgpainless.key.protection.fixes.S2KUsageFix; -import org.pgpainless.key.protection.passphrase_provider.SolitaryPassphraseProvider; -import org.pgpainless.key.util.KeyRingUtils; -import org.pgpainless.key.util.RevocationAttributes; -import org.pgpainless.signature.SignatureUtils; -import org.pgpainless.signature.subpackets.SignatureSubpacketGeneratorUtil; -import org.pgpainless.util.Passphrase; -import org.pgpainless.util.selection.userid.SelectUserId; - -public class SecretKeyRingEditor implements SecretKeyRingEditorInterface { - - // Default algorithm for calculating private key checksums - // While I'd like to use something else, eg. SHA256, BC seems to lack support for - // calculating secret key checksums with algorithms other than SHA1. - private static final HashAlgorithm defaultDigestHashAlgorithm = HashAlgorithm.SHA1; - - private PGPSecretKeyRing secretKeyRing; - - public SecretKeyRingEditor(PGPSecretKeyRing secretKeyRing) { - if (secretKeyRing == null) { - throw new NullPointerException("SecretKeyRing MUST NOT be null."); - } - this.secretKeyRing = secretKeyRing; - } - - @Override - public SecretKeyRingEditorInterface addUserId(String userId, SecretKeyRingProtector secretKeyRingProtector) throws PGPException { - userId = sanitizeUserId(userId); - - List secretKeyList = new ArrayList<>(); - Iterator secretKeyIterator = secretKeyRing.getSecretKeys(); - - // add user-id certificate to primary key - PGPSecretKey primaryKey = secretKeyIterator.next(); - PGPPublicKey publicKey = primaryKey.getPublicKey(); - PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(primaryKey, secretKeyRingProtector); - publicKey = addUserIdToPubKey(userId, privateKey, publicKey); - primaryKey = PGPSecretKey.replacePublicKey(primaryKey, publicKey); - - secretKeyList.add(primaryKey); - - while (secretKeyIterator.hasNext()) { - secretKeyList.add(secretKeyIterator.next()); - } - - secretKeyRing = new PGPSecretKeyRing(secretKeyList); - - return this; - } - - private static PGPPublicKey addUserIdToPubKey(String userId, PGPPrivateKey privateKey, PGPPublicKey publicKey) throws PGPException { - if (privateKey.getKeyID() != publicKey.getKeyID()) { - throw new IllegalArgumentException("Key-ID mismatch!"); - } - // Create signature with new user-id and add it to the public key - PGPSignatureGenerator signatureGenerator = SignatureUtils.getSignatureGeneratorFor(publicKey); - signatureGenerator.init(SignatureType.POSITIVE_CERTIFICATION.getCode(), privateKey); - - PGPSignature userIdSignature = signatureGenerator.generateCertification(userId, publicKey); - publicKey = PGPPublicKey.addCertification(publicKey, - userId, userIdSignature); - - return publicKey; - } - - // TODO: Move to utility class? - private String sanitizeUserId(String userId) { - userId = userId.trim(); - // TODO: Further research how to sanitize user IDs. - // eg. what about newlines? - return userId; - } - - @Override - public SecretKeyRingEditorInterface deleteUserId(String userId, SecretKeyRingProtector protector) { - return deleteUserIds(SelectUserId.exactMatch(userId), protector); - } - - @Override - public SecretKeyRingEditorInterface deleteUserIds(SelectUserId selectionStrategy, SecretKeyRingProtector secretKeyRingProtector) { - List publicKeys = new ArrayList<>(); - Iterator publicKeyIterator = secretKeyRing.getPublicKeys(); - PGPPublicKey primaryKey = publicKeyIterator.next(); - List matchingUserIds = selectionStrategy.selectUserIds(iteratorToList(primaryKey.getUserIDs())); - if (matchingUserIds.isEmpty()) { - throw new NoSuchElementException("Key does not have a matching user-id attribute."); - } - for (String userId : matchingUserIds) { - primaryKey = PGPPublicKey.removeCertification(primaryKey, userId); - } - publicKeys.add(primaryKey); - - while (publicKeyIterator.hasNext()) { - publicKeys.add(publicKeyIterator.next()); - } - - PGPPublicKeyRing publicKeyRing = new PGPPublicKeyRing(publicKeys); - secretKeyRing = PGPSecretKeyRing.replacePublicKeys(secretKeyRing, publicKeyRing); - return this; - } - - @Override - public SecretKeyRingEditorInterface addSubKey(@Nonnull KeySpec keySpec, - @Nonnull Passphrase subKeyPassphrase, - SecretKeyRingProtector secretKeyRingProtector) - throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { - - PGPSecretKey secretSubKey = generateSubKey(keySpec, subKeyPassphrase); - SecretKeyRingProtector subKeyProtector = PasswordBasedSecretKeyRingProtector - .forKey(secretSubKey, subKeyPassphrase); - PGPSignatureSubpacketVector hashedSubpackets = keySpec.getSubpackets(); - PGPSignatureSubpacketVector unhashedSubpackets = null; - - return addSubKey(secretSubKey, hashedSubpackets, unhashedSubpackets, subKeyProtector, secretKeyRingProtector); - } - - @Override - public SecretKeyRingEditorInterface addSubKey(PGPSecretKey secretSubKey, - PGPSignatureSubpacketVector hashedSubpackets, - PGPSignatureSubpacketVector unhashedSubpackets, - SecretKeyRingProtector subKeyProtector, - SecretKeyRingProtector keyRingProtector) - throws PGPException { - - PGPPublicKey primaryKey = secretKeyRing.getSecretKey().getPublicKey(); - - PBESecretKeyDecryptor ringDecryptor = keyRingProtector.getDecryptor(primaryKey.getKeyID()); - PBESecretKeyEncryptor subKeyEncryptor = subKeyProtector.getEncryptor(secretSubKey.getKeyID()); - - PGPDigestCalculator digestCalculator = - ImplementationFactory.getInstance().getPGPDigestCalculator(defaultDigestHashAlgorithm); - PGPContentSignerBuilder contentSignerBuilder = ImplementationFactory.getInstance() - .getPGPContentSignerBuilder( - primaryKey.getAlgorithm(), - HashAlgorithm.SHA256.getAlgorithmId() // TODO: Why SHA256? - ); - - PGPPrivateKey privateSubKey = UnlockSecretKey.unlockSecretKey(secretSubKey, subKeyProtector); - PGPKeyPair subKeyPair = new PGPKeyPair(secretSubKey.getPublicKey(), privateSubKey); - - PGPKeyRingGenerator keyRingGenerator = new PGPKeyRingGenerator( - secretKeyRing, ringDecryptor, digestCalculator, contentSignerBuilder, subKeyEncryptor); - - keyRingGenerator.addSubKey(subKeyPair, hashedSubpackets, unhashedSubpackets); - secretKeyRing = keyRingGenerator.generateSecretKeyRing(); - - return this; - } - - private PGPSecretKey generateSubKey(@Nonnull KeySpec keySpec, - @Nonnull Passphrase subKeyPassphrase) - throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - PGPDigestCalculator checksumCalculator = ImplementationFactory.getInstance() - .getPGPDigestCalculator(defaultDigestHashAlgorithm); - - PBESecretKeyEncryptor subKeyEncryptor = subKeyPassphrase.isEmpty() ? null : - ImplementationFactory.getInstance().getPBESecretKeyEncryptor(SymmetricKeyAlgorithm.AES_256, subKeyPassphrase); - - PGPKeyPair keyPair = KeyRingBuilder.generateKeyPair(keySpec); - PGPSecretKey secretKey = new PGPSecretKey(keyPair.getPrivateKey(), keyPair.getPublicKey(), - checksumCalculator, false, subKeyEncryptor); - return secretKey; - } - - @Override - public SecretKeyRingEditorInterface deleteSubKey(OpenPgpV4Fingerprint fingerprint, - SecretKeyRingProtector protector) { - return deleteSubKey(fingerprint.getKeyId(), protector); - } - - @Override - public SecretKeyRingEditorInterface deleteSubKey(long subKeyId, - SecretKeyRingProtector protector) { - if (secretKeyRing.getSecretKey().getKeyID() == subKeyId) { - throw new IllegalArgumentException("You cannot delete the primary key of this key ring."); - } - - PGPSecretKey deleteMe = secretKeyRing.getSecretKey(subKeyId); - if (deleteMe == null) { - throw new NoSuchElementException("KeyRing does not contain a key with keyId " + Long.toHexString(subKeyId)); - } - - PGPSecretKeyRing newKeyRing = PGPSecretKeyRing.removeSecretKey(secretKeyRing, deleteMe); - secretKeyRing = newKeyRing; - return this; - } - - @Override - public SecretKeyRingEditorInterface revoke(SecretKeyRingProtector secretKeyRingProtector, - RevocationAttributes revocationAttributes) - throws PGPException { - return revokeSubKey(secretKeyRing.getSecretKey().getKeyID(), secretKeyRingProtector, revocationAttributes); - } - - @Override - public SecretKeyRingEditorInterface revokeSubKey(OpenPgpV4Fingerprint fingerprint, - SecretKeyRingProtector protector, - RevocationAttributes revocationAttributes) - throws PGPException { - return revokeSubKey(fingerprint.getKeyId(), protector, revocationAttributes); - } - - @Override - public SecretKeyRingEditorInterface revokeSubKey(long subKeyId, - SecretKeyRingProtector protector, - RevocationAttributes revocationAttributes) - throws PGPException { - PGPPublicKey revokeeSubKey = secretKeyRing.getPublicKey(subKeyId); - if (revokeeSubKey == null) { - throw new NoSuchElementException("No subkey with id " + Long.toHexString(subKeyId) + " found."); - } - - secretKeyRing = revokeSubKey(protector, revokeeSubKey, revocationAttributes); - return this; - } - - @Override - public SecretKeyRingEditorInterface revokeUserId(String userId, - SecretKeyRingProtector secretKeyRingProtector, - RevocationAttributes revocationAttributes) - throws PGPException { - Iterator userIds = secretKeyRing.getPublicKey().getUserIDs(); - boolean found = false; - while (userIds.hasNext()) { - if (userId.equals(userIds.next())) { - found = true; - break; - } - } - if (!found) { - throw new NoSuchElementException("No user-id '" + userId + "' found on the key."); - } - return doRevokeUserId(userId, secretKeyRingProtector, revocationAttributes); - } - - private SecretKeyRingEditorInterface doRevokeUserId(String userId, - SecretKeyRingProtector protector, - RevocationAttributes revocationAttributes) throws PGPException { - PGPSecretKey primarySecretKey = secretKeyRing.getSecretKey(); - PGPPublicKey primaryPublicKey = primarySecretKey.getPublicKey(); - PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(primarySecretKey, protector); - - PGPSignatureSubpacketGenerator subpacketGenerator = new PGPSignatureSubpacketGenerator(); - subpacketGenerator.setSignatureCreationTime(false, new Date()); - subpacketGenerator.setRevocable(false, false); - subpacketGenerator.setIssuerFingerprint(false, primarySecretKey); - if (revocationAttributes != null) { - RevocationAttributes.Reason reason = revocationAttributes.getReason(); - if (reason != RevocationAttributes.Reason.NO_REASON - && reason != RevocationAttributes.Reason.USER_ID_NO_LONGER_VALID) { - throw new IllegalArgumentException("Revocation reason must either be NO_REASON or USER_ID_NO_LONGER_VALID"); - } - subpacketGenerator.setRevocationReason(false, revocationAttributes.getReason().code(), revocationAttributes.getDescription()); - } - - PGPSignatureGenerator signatureGenerator = SignatureUtils.getSignatureGeneratorFor(primarySecretKey); - signatureGenerator.setHashedSubpackets(subpacketGenerator.generate()); - signatureGenerator.init(SignatureType.CERTIFICATION_REVOCATION.getCode(), privateKey); - - PGPSignature revocationSignature = signatureGenerator.generateCertification(userId, primaryPublicKey); - primaryPublicKey = PGPPublicKey.addCertification(primaryPublicKey, userId, revocationSignature); - - PGPPublicKeyRing publicKeyRing = KeyRingUtils.publicKeyRingFrom(secretKeyRing); - publicKeyRing = PGPPublicKeyRing.insertPublicKey(publicKeyRing, primaryPublicKey); - secretKeyRing = PGPSecretKeyRing.replacePublicKeys(secretKeyRing, publicKeyRing); - - return this; - } - - @Override - public SecretKeyRingEditorInterface setExpirationDate(Date expiration, - SecretKeyRingProtector secretKeyRingProtector) - throws PGPException { - return setExpirationDate(new OpenPgpV4Fingerprint(secretKeyRing), expiration, secretKeyRingProtector); - } - - @Override - public SecretKeyRingEditorInterface setExpirationDate(OpenPgpV4Fingerprint fingerprint, - Date expiration, - SecretKeyRingProtector secretKeyRingProtector) - throws PGPException { - - List secretKeyList = new ArrayList<>(); - PGPSecretKey primaryKey = secretKeyRing.getSecretKey(); - if (!primaryKey.isMasterKey()) { - throw new IllegalArgumentException("Key Ring does not appear to contain a primary secret key."); - } - - boolean found = false; - for (PGPSecretKey secretKey : secretKeyRing) { - // Skip over unaffected subkeys - if (secretKey.getKeyID() != fingerprint.getKeyId()) { - secretKeyList.add(secretKey); - continue; - } - // We found the target subkey - found = true; - secretKey = setExpirationDate(primaryKey, secretKey, expiration, secretKeyRingProtector); - secretKeyList.add(secretKey); - } - - if (!found) { - throw new IllegalArgumentException("Key Ring does not contain secret key with fingerprint " + fingerprint); - } - - secretKeyRing = new PGPSecretKeyRing(secretKeyList); - - return this; - } - - private PGPSecretKey setExpirationDate(PGPSecretKey primaryKey, - PGPSecretKey subjectKey, - Date expiration, - SecretKeyRingProtector secretKeyRingProtector) - throws PGPException { - - if (expiration != null && expiration.before(subjectKey.getPublicKey().getCreationTime())) { - throw new IllegalArgumentException("Expiration date cannot be before creation date."); - } - - PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(primaryKey, secretKeyRingProtector); - PGPPublicKey subjectPubKey = subjectKey.getPublicKey(); - - PGPSignature oldSignature = getPreviousSignature(primaryKey, subjectPubKey); - - PGPSignatureSubpacketVector oldSubpackets = oldSignature.getHashedSubPackets(); - PGPSignatureSubpacketGenerator subpacketGenerator = new PGPSignatureSubpacketGenerator(oldSubpackets); - SignatureSubpacketGeneratorUtil.setSignatureCreationTimeInSubpacketGenerator(new Date(), subpacketGenerator); - SignatureSubpacketGeneratorUtil.setExpirationDateInSubpacketGenerator(expiration, subjectPubKey.getCreationTime(), subpacketGenerator); - - PGPSignatureGenerator signatureGenerator = SignatureUtils.getSignatureGeneratorFor(primaryKey); - signatureGenerator.setHashedSubpackets(subpacketGenerator.generate()); - - if (primaryKey.getKeyID() == subjectKey.getKeyID()) { - signatureGenerator.init(PGPSignature.POSITIVE_CERTIFICATION, privateKey); - - for (Iterator it = subjectKey.getUserIDs(); it.hasNext(); ) { - String userId = it.next(); - PGPSignature signature = signatureGenerator.generateCertification(userId, subjectPubKey); - subjectPubKey = PGPPublicKey.addCertification(subjectPubKey, userId, signature); - } - } else { - signatureGenerator.init(PGPSignature.SUBKEY_BINDING, privateKey); - - PGPSignature signature = signatureGenerator.generateCertification(primaryKey.getPublicKey(), subjectPubKey); - subjectPubKey = PGPPublicKey.addCertification(subjectPubKey, signature); - } - - subjectKey = PGPSecretKey.replacePublicKey(subjectKey, subjectPubKey); - return subjectKey; - } - - private PGPSignature getPreviousSignature(PGPSecretKey primaryKey, PGPPublicKey subjectPubKey) { - PGPSignature oldSignature = null; - if (primaryKey.getKeyID() == subjectPubKey.getKeyID()) { - Iterator keySignatures = subjectPubKey.getSignaturesForKeyID(primaryKey.getKeyID()); - while (keySignatures.hasNext()) { - PGPSignature next = keySignatures.next(); - SignatureType type = SignatureType.valueOf(next.getSignatureType()); - if (type == SignatureType.POSITIVE_CERTIFICATION || - type == SignatureType.CASUAL_CERTIFICATION || - type == SignatureType.GENERIC_CERTIFICATION) { - oldSignature = next; - } - } - if (oldSignature == null) { - throw new IllegalStateException("Key " + new OpenPgpV4Fingerprint(subjectPubKey) + " does not have a previous positive/casual/generic certification signature."); - } - } else { - Iterator bindingSignatures = subjectPubKey.getSignaturesOfType(SignatureType.SUBKEY_BINDING.getCode()); - while (bindingSignatures.hasNext()) { - oldSignature = bindingSignatures.next(); - } - } - - if (oldSignature == null) { - throw new IllegalStateException("Key " + new OpenPgpV4Fingerprint(subjectPubKey) + " does not have a previous subkey binding signature."); - } - return oldSignature; - } - - @Override - public PGPSignature createRevocationCertificate(SecretKeyRingProtector secretKeyRingProtector, - RevocationAttributes revocationAttributes) - throws PGPException { - PGPPublicKey revokeeSubKey = secretKeyRing.getPublicKey(); - PGPSignature revocationCertificate = generateRevocation(secretKeyRingProtector, revokeeSubKey, revocationAttributes); - return revocationCertificate; - } - - @Override - public PGPSignature createRevocationCertificate(long subkeyId, SecretKeyRingProtector secretKeyRingProtector, RevocationAttributes revocationAttributes) throws PGPException { - PGPPublicKey revokeeSubKey = KeyRingUtils.requirePublicKeyFrom(secretKeyRing, subkeyId); - PGPSignature revocationCertificate = generateRevocation(secretKeyRingProtector, revokeeSubKey, revocationAttributes); - return revocationCertificate; - } - - private PGPSecretKeyRing revokeSubKey(SecretKeyRingProtector protector, - PGPPublicKey revokeeSubKey, - RevocationAttributes revocationAttributes) - throws PGPException { - PGPSignature subKeyRevocation = generateRevocation(protector, revokeeSubKey, revocationAttributes); - revokeeSubKey = PGPPublicKey.addCertification(revokeeSubKey, subKeyRevocation); - - // Inject revoked public key into key ring - PGPPublicKeyRing publicKeyRing = KeyRingUtils.publicKeyRingFrom(secretKeyRing); - publicKeyRing = PGPPublicKeyRing.insertPublicKey(publicKeyRing, revokeeSubKey); - return PGPSecretKeyRing.replacePublicKeys(secretKeyRing, publicKeyRing); - } - - private PGPSignature generateRevocation(SecretKeyRingProtector protector, - PGPPublicKey revokeeSubKey, - RevocationAttributes revocationAttributes) - throws PGPException { - PGPSecretKey primaryKey = secretKeyRing.getSecretKey(); - PGPSignatureGenerator signatureGenerator = SignatureUtils.getSignatureGeneratorFor(primaryKey); - PGPSignatureSubpacketGenerator subpacketGenerator = new PGPSignatureSubpacketGenerator(); - subpacketGenerator.setIssuerFingerprint(false, primaryKey); - - if (revocationAttributes != null) { - subpacketGenerator.setRevocationReason(false, revocationAttributes.getReason().code(), revocationAttributes.getDescription()); - } - - PGPSignatureSubpacketVector subPackets = subpacketGenerator.generate(); - signatureGenerator.setHashedSubpackets(subPackets); - - PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(primaryKey, protector); - - PGPSignature revocation; - if (revokeeSubKey.isMasterKey()) { - signatureGenerator.init(SignatureType.KEY_REVOCATION.getCode(), privateKey); - revocation = signatureGenerator.generateCertification(revokeeSubKey); - } else { - signatureGenerator.init(SignatureType.SUBKEY_REVOCATION.getCode(), privateKey); - revocation = signatureGenerator.generateCertification(primaryKey.getPublicKey(), revokeeSubKey); - } - return revocation; - } - - @Override - public WithKeyRingEncryptionSettings changePassphraseFromOldPassphrase(@Nullable Passphrase oldPassphrase, - @Nonnull KeyRingProtectionSettings oldProtectionSettings) { - SecretKeyRingProtector protector = new PasswordBasedSecretKeyRingProtector( - oldProtectionSettings, - new SolitaryPassphraseProvider(oldPassphrase)); - - return new WithKeyRingEncryptionSettingsImpl(null, protector); - } - - @Override - public WithKeyRingEncryptionSettings changeSubKeyPassphraseFromOldPassphrase(@Nonnull Long keyId, - @Nullable Passphrase oldPassphrase, - @Nonnull KeyRingProtectionSettings oldProtectionSettings) { - Map passphraseMap = Collections.singletonMap(keyId, oldPassphrase); - SecretKeyRingProtector protector = new CachingSecretKeyRingProtector( - passphraseMap, oldProtectionSettings, null); - - return new WithKeyRingEncryptionSettingsImpl(keyId, protector); - } - - @Override - public PGPSecretKeyRing done() { - return secretKeyRing; - } - - private final class WithKeyRingEncryptionSettingsImpl implements WithKeyRingEncryptionSettings { - - private final Long keyId; - // Protector to unlock the key with the old passphrase - private final SecretKeyRingProtector oldProtector; - - /** - * Builder for selecting protection settings. - * - * If the keyId is null, the whole keyRing will get the same new passphrase. - * - * @param keyId id of the subkey whose passphrase will be changed, or null. - * @param oldProtector protector do unlock the key/ring. - */ - private WithKeyRingEncryptionSettingsImpl(Long keyId, SecretKeyRingProtector oldProtector) { - this.keyId = keyId; - this.oldProtector = oldProtector; - } - - @Override - public WithPassphrase withSecureDefaultSettings() { - return withCustomSettings(KeyRingProtectionSettings.secureDefaultSettings()); - } - - @Override - public WithPassphrase withCustomSettings(KeyRingProtectionSettings settings) { - return new WithPassphraseImpl(keyId, oldProtector, settings); - } - } - - private final class WithPassphraseImpl implements WithPassphrase { - - private final SecretKeyRingProtector oldProtector; - private final KeyRingProtectionSettings newProtectionSettings; - private final Long keyId; - - private WithPassphraseImpl(Long keyId, SecretKeyRingProtector oldProtector, KeyRingProtectionSettings newProtectionSettings) { - this.keyId = keyId; - this.oldProtector = oldProtector; - this.newProtectionSettings = newProtectionSettings; - } - - @Override - public SecretKeyRingEditorInterface toNewPassphrase(Passphrase passphrase) throws PGPException { - SecretKeyRingProtector newProtector = new PasswordBasedSecretKeyRingProtector( - newProtectionSettings, new SolitaryPassphraseProvider(passphrase)); - - PGPSecretKeyRing secretKeys = changePassphrase(keyId, SecretKeyRingEditor.this.secretKeyRing, oldProtector, newProtector); - SecretKeyRingEditor.this.secretKeyRing = secretKeys; - - return SecretKeyRingEditor.this; - } - - @Override - public SecretKeyRingEditorInterface toNoPassphrase() throws PGPException { - SecretKeyRingProtector newProtector = new UnprotectedKeysProtector(); - - PGPSecretKeyRing secretKeys = changePassphrase(keyId, SecretKeyRingEditor.this.secretKeyRing, oldProtector, newProtector); - SecretKeyRingEditor.this.secretKeyRing = secretKeys; - - return SecretKeyRingEditor.this; - } - } - - private PGPSecretKeyRing changePassphrase(Long keyId, - PGPSecretKeyRing secretKeys, - SecretKeyRingProtector oldProtector, - SecretKeyRingProtector newProtector) throws PGPException { - List secretKeyList = new ArrayList<>(); - if (keyId == null) { - // change passphrase of whole key ring - Iterator secretKeyIterator = secretKeys.getSecretKeys(); - while (secretKeyIterator.hasNext()) { - PGPSecretKey secretKey = secretKeyIterator.next(); - secretKey = reencryptPrivateKey(secretKey, oldProtector, newProtector); - secretKeyList.add(secretKey); - } - } else { - // change passphrase of selected subkey only - Iterator secretKeyIterator = secretKeys.getSecretKeys(); - while (secretKeyIterator.hasNext()) { - PGPSecretKey secretKey = secretKeyIterator.next(); - if (secretKey.getPublicKey().getKeyID() == keyId) { - // Re-encrypt only the selected subkey - secretKey = reencryptPrivateKey(secretKey, oldProtector, newProtector); - } - secretKeyList.add(secretKey); - } - } - - PGPSecretKeyRing newRing = new PGPSecretKeyRing(secretKeyList); - newRing = s2kUsageFixIfNecessary(newRing, newProtector); - return newRing; - } - - private PGPSecretKeyRing s2kUsageFixIfNecessary(PGPSecretKeyRing secretKeys, SecretKeyRingProtector protector) throws PGPException { - boolean hasS2KUsageChecksum = false; - for (PGPSecretKey secKey : secretKeys) { - if (secKey.getS2KUsage() == SecretKeyPacket.USAGE_CHECKSUM) { - hasS2KUsageChecksum = true; - break; - } - } - if (hasS2KUsageChecksum) { - secretKeys = S2KUsageFix.replaceUsageChecksumWithUsageSha1(secretKeys, protector, true); - } - return secretKeys; - } - - private static PGPSecretKey reencryptPrivateKey(PGPSecretKey secretKey, SecretKeyRingProtector oldProtector, SecretKeyRingProtector newProtector) throws PGPException { - S2K s2k = secretKey.getS2K(); - // If the key uses GNU_DUMMY_S2K, we leave it as is and skip this block - if (s2k == null || s2k.getType() != S2K.GNU_DUMMY_S2K) { - long secretKeyId = secretKey.getKeyID(); - PBESecretKeyDecryptor decryptor = oldProtector.getDecryptor(secretKeyId); - PBESecretKeyEncryptor encryptor = newProtector.getEncryptor(secretKeyId); - secretKey = PGPSecretKey.copyWithNewPassword(secretKey, decryptor, encryptor); - } - return secretKey; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.java b/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.java deleted file mode 100644 index 5b8f329f..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.java +++ /dev/null @@ -1,363 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.modification.secretkeyring; - -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; -import java.util.Date; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.PGPSignatureSubpacketVector; -import org.pgpainless.key.OpenPgpV4Fingerprint; -import org.pgpainless.key.generation.KeySpec; -import org.pgpainless.key.protection.KeyRingProtectionSettings; -import org.pgpainless.key.protection.SecretKeyRingProtector; -import org.pgpainless.key.util.RevocationAttributes; -import org.pgpainless.key.util.UserId; -import org.pgpainless.util.Passphrase; -import org.pgpainless.util.selection.userid.SelectUserId; - -public interface SecretKeyRingEditorInterface { - - /** - * Add a user-id to the key ring. - * - * @param userId user-id - * @param secretKeyRingProtector protector to unlock the secret key - * @return the builder - */ - default SecretKeyRingEditorInterface addUserId(UserId userId, SecretKeyRingProtector secretKeyRingProtector) throws PGPException { - return addUserId(userId.toString(), secretKeyRingProtector); - } - - /** - * Add a user-id to the key ring. - * - * @param userId user-id - * @param secretKeyRingProtector protector to unlock the secret key - * @return the builder - */ - SecretKeyRingEditorInterface addUserId(String userId, SecretKeyRingProtector secretKeyRingProtector) throws PGPException; - - /** - * Remove a user-id from the key ring. - * - * @param userId exact user-id to be removed - * @param secretKeyRingProtector protector to unlock the secret key - * @return the builder - */ - SecretKeyRingEditorInterface deleteUserId(String userId, SecretKeyRingProtector secretKeyRingProtector); - - /** - * Remove a user-id from the key ring. - * - * @param userId exact user-id to be removed - * @param secretKeyRingProtector protector to unlock the secret key - * @return the builder - */ - default SecretKeyRingEditorInterface deleteUserId(UserId userId, SecretKeyRingProtector secretKeyRingProtector) { - return deleteUserId(userId.toString(), secretKeyRingProtector); - } - - /** - * Delete all user-ids from the key, which match the provided {@link SelectUserId} strategy. - * - * @param selectionStrategy strategy to select user-ids - * @param secretKeyRingProtector protector to unlock the secret key - * @return the builder - */ - SecretKeyRingEditorInterface deleteUserIds(SelectUserId selectionStrategy, SecretKeyRingProtector secretKeyRingProtector); - - /** - * Add a subkey to the key ring. - * The subkey will be generated from the provided {@link KeySpec}. - * - * @param keySpec key specification - * @param subKeyPassphrase passphrase to encrypt the sub key - * @param secretKeyRingProtector protector to unlock the secret key of the key ring - * @return the builder - */ - SecretKeyRingEditorInterface addSubKey(@Nonnull KeySpec keySpec, - @Nonnull Passphrase subKeyPassphrase, - SecretKeyRingProtector secretKeyRingProtector) - throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException; - - SecretKeyRingEditorInterface addSubKey(PGPSecretKey subKey, - PGPSignatureSubpacketVector hashedSubpackets, - PGPSignatureSubpacketVector unhashedSubpackets, - SecretKeyRingProtector subKeyProtector, SecretKeyRingProtector keyRingProtector) - throws PGPException; - - /** - * Delete a subkey from the key ring. - * The subkey with the provided fingerprint will be remove from the key ring. - * If no suitable subkey is found, a {@link java.util.NoSuchElementException} will be thrown. - * - * @param fingerprint fingerprint of the subkey to be removed - * @param secretKeyRingProtector protector to unlock the secret key ring - * @return the builder - */ - SecretKeyRingEditorInterface deleteSubKey(OpenPgpV4Fingerprint fingerprint, SecretKeyRingProtector secretKeyRingProtector); - - /** - * Delete a subkey from the key ring. - * The subkey with the provided key-id will be removed from the key ring. - * If no suitable subkey is found, a {@link java.util.NoSuchElementException} will be thrown. - * - * @param subKeyId id of the subkey - * @param secretKeyRingProtector protector to unlock the secret key ring - * @return the builder - */ - SecretKeyRingEditorInterface deleteSubKey(long subKeyId, SecretKeyRingProtector secretKeyRingProtector); - - /** - * Revoke the key ring. - * The revocation will be a hard revocation, rendering the whole key invalid for any past or future signatures. - * - * @param secretKeyRingProtector protector of the primary key - * @return the builder - */ - default SecretKeyRingEditorInterface revoke(SecretKeyRingProtector secretKeyRingProtector) - throws PGPException { - return revoke(secretKeyRingProtector, null); - } - - /** - * Revoke the key ring using the provided revocation attributes. - * The attributes define, whether or not the revocation was a hard revocation or not. - * - * @param secretKeyRingProtector protector of the primary key - * @param revocationAttributes reason for the revocation - * @return the builder - */ - SecretKeyRingEditorInterface revoke(SecretKeyRingProtector secretKeyRingProtector, - RevocationAttributes revocationAttributes) - throws PGPException; - - /** - * Revoke the subkey binding signature of a subkey. - * The subkey with the provided fingerprint will be revoked. - * If no suitable subkey is found, a {@link java.util.NoSuchElementException} will be thrown. - * - * @param fingerprint fingerprint of the subkey to be revoked - * @param secretKeyRingProtector protector to unlock the secret key ring - * @return the builder - */ - default SecretKeyRingEditorInterface revokeSubKey(OpenPgpV4Fingerprint fingerprint, - SecretKeyRingProtector secretKeyRingProtector) - throws PGPException { - return revokeSubKey(fingerprint, secretKeyRingProtector, null); - } - - /** - * Revoke the subkey binding signature of a subkey. - * The subkey with the provided fingerprint will be revoked. - * If no suitable subkey is found, a {@link java.util.NoSuchElementException} will be thrown. - * - * @param fingerprint fingerprint of the subkey to be revoked - * @param secretKeyRingProtector protector to unlock the primary key - * @param revocationAttributes reason for the revocation - * @return the builder - */ - SecretKeyRingEditorInterface revokeSubKey(OpenPgpV4Fingerprint fingerprint, - SecretKeyRingProtector secretKeyRingProtector, - RevocationAttributes revocationAttributes) - throws PGPException; - - /** - * Revoke the subkey binding signature of a subkey. - * The subkey with the provided key-id will be revoked. - * If no suitable subkey is found, q {@link java.util.NoSuchElementException} will be thrown. - * - * @param subKeyId id of the subkey - * @param secretKeyRingProtector protector to unlock the secret key ring - * @return the builder - */ - default SecretKeyRingEditorInterface revokeSubKey(long subKeyId, - SecretKeyRingProtector secretKeyRingProtector) - throws PGPException { - return revokeSubKey(subKeyId, secretKeyRingProtector, null); - } - - /** - * Revoke the subkey binding signature of a subkey. - * The subkey with the provided key-id will be revoked. - * If no suitable subkey is found, q {@link java.util.NoSuchElementException} will be thrown. - * - * @param subKeyId id of the subkey - * @param secretKeyRingProtector protector to unlock the primary key - * @param revocationAttributes reason for the revocation - * @return the builder - */ - SecretKeyRingEditorInterface revokeSubKey(long subKeyId, - SecretKeyRingProtector secretKeyRingProtector, - RevocationAttributes revocationAttributes) - throws PGPException; - - - /** - * Revoke the given userID. - * The revocation will be a hard revocation, rendering the user-id invalid for any past or future signatures. - * - * @param userId userId to revoke - * @param secretKeyRingProtector protector to unlock the primary key - * @return the builder - */ - default SecretKeyRingEditorInterface revokeUserId(String userId, - SecretKeyRingProtector secretKeyRingProtector) - throws PGPException { - return revokeUserId(userId, secretKeyRingProtector, null); - } - - /** - * Revoke the given userID using the provided revocation attributes. - * - * @param userId userId to revoke - * @param secretKeyRingProtector protector to unlock the primary key - * @param revocationAttributes reason for the revocation - * @return the builder - */ - SecretKeyRingEditorInterface revokeUserId(String userId, - SecretKeyRingProtector secretKeyRingProtector, - RevocationAttributes revocationAttributes) - throws PGPException; - - /** - * Set the expiration date for the primary key of the key ring. - * If the key is supposed to never expire, then an expiration date of null is expected. - * - * @param expiration new expiration date or null - * @param secretKeyRingProtector to unlock the secret key - * @return the builder - */ - SecretKeyRingEditorInterface setExpirationDate(Date expiration, - SecretKeyRingProtector secretKeyRingProtector) - throws PGPException; - - /** - * Set key expiration time. - * - * @param fingerprint key that will have its expiration date adjusted - * @param expiration target expiration time or @{code null} for no expiration - * @param secretKeyRingProtector protector to unlock the priary key - * @return the builder - */ - SecretKeyRingEditorInterface setExpirationDate(OpenPgpV4Fingerprint fingerprint, - Date expiration, - SecretKeyRingProtector secretKeyRingProtector) - throws PGPException; - - /** - * Create a detached revocation certificate, which can be used to revoke the specified key. - * - * @param secretKeyRingProtector protector to unlock the primary key. - * @param revocationAttributes reason for the revocation - * @return revocation certificate - */ - PGPSignature createRevocationCertificate(SecretKeyRingProtector secretKeyRingProtector, - RevocationAttributes revocationAttributes) - throws PGPException; - - PGPSignature createRevocationCertificate(long subkeyId, - SecretKeyRingProtector secretKeyRingProtector, - RevocationAttributes revocationAttributes) - throws PGPException; - - default PGPSignature createRevocationCertificate(OpenPgpV4Fingerprint subkeyFingerprint, - SecretKeyRingProtector secretKeyRingProtector, - RevocationAttributes revocationAttributes) - throws PGPException { - return createRevocationCertificate(subkeyFingerprint.getKeyId(), secretKeyRingProtector, revocationAttributes); - } - - /** - * Change the passphrase of the whole key ring. - * - * @param oldPassphrase old passphrase or null, if the key was unprotected - * @return next builder step - */ - default WithKeyRingEncryptionSettings changePassphraseFromOldPassphrase(@Nullable Passphrase oldPassphrase) { - return changePassphraseFromOldPassphrase(oldPassphrase, KeyRingProtectionSettings.secureDefaultSettings()); - } - - /** - * Change the passphrase of the whole key ring. - * - * @param oldPassphrase old passphrase or null, if the key was unprotected - * @param oldProtectionSettings custom settings for the old passphrase - * @return next builder step - */ - WithKeyRingEncryptionSettings changePassphraseFromOldPassphrase(@Nullable Passphrase oldPassphrase, - @Nonnull KeyRingProtectionSettings oldProtectionSettings); - - /** - * Change the passphrase of a single subkey in the key ring. - * - * Note: While it is a valid use-case to have different passphrases per subKey, - * this is one of the reasons why OpenPGP sucks in practice. - * - * @param keyId id of the subkey - * @param oldPassphrase old passphrase - * @return next builder step - */ - default WithKeyRingEncryptionSettings changeSubKeyPassphraseFromOldPassphrase(@Nonnull Long keyId, - @Nullable Passphrase oldPassphrase) { - return changeSubKeyPassphraseFromOldPassphrase(keyId, oldPassphrase, KeyRingProtectionSettings.secureDefaultSettings()); - } - - WithKeyRingEncryptionSettings changeSubKeyPassphraseFromOldPassphrase(@Nonnull Long keyId, - @Nullable Passphrase oldPassphrase, - @Nonnull KeyRingProtectionSettings oldProtectionSettings); - - interface WithKeyRingEncryptionSettings { - - /** - * Set secure default settings for the symmetric passphrase encryption. - * Note that this obviously has no effect if you decide to set {@link WithPassphrase#toNoPassphrase()}. - * - * @return next builder step - */ - WithPassphrase withSecureDefaultSettings(); - - /** - * Set custom settings for the symmetric passphrase encryption. - * - * @param settings custom settings - * @return next builder step - */ - WithPassphrase withCustomSettings(KeyRingProtectionSettings settings); - - } - - interface WithPassphrase { - - /** - * Set the passphrase. - * - * @param passphrase passphrase - * @return editor builder - */ - SecretKeyRingEditorInterface toNewPassphrase(Passphrase passphrase) throws PGPException; - - /** - * Leave the key unprotected. - * - * @return editor builder - */ - SecretKeyRingEditorInterface toNoPassphrase() throws PGPException; - } - - /** - * Return the {@link PGPSecretKeyRing}. - * @return the key - */ - PGPSecretKeyRing done(); - -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/package-info.java deleted file mode 100644 index 6b3eb3b3..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Classes that deal with modifications made to {@link org.bouncycastle.openpgp.PGPSecretKeyRing PGPSecretKeyRings}. - */ -package org.pgpainless.key.modification.secretkeyring; diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/parsing/KeyRingReader.java b/pgpainless-core/src/main/java/org/pgpainless/key/parsing/KeyRingReader.java deleted file mode 100644 index 585d3eb3..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/parsing/KeyRingReader.java +++ /dev/null @@ -1,200 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.parsing; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.Charset; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import javax.annotation.Nonnull; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPMarker; -import org.bouncycastle.openpgp.PGPObjectFactory; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; -import org.bouncycastle.util.io.Streams; -import org.pgpainless.implementation.ImplementationFactory; -import org.pgpainless.key.collection.PGPKeyRingCollection; -import org.pgpainless.util.ArmorUtils; - -public class KeyRingReader { - - public static final Charset UTF8 = Charset.forName("UTF-8"); - - public PGPPublicKeyRing publicKeyRing(@Nonnull InputStream inputStream) throws IOException { - return readPublicKeyRing(inputStream); - } - - public PGPPublicKeyRing publicKeyRing(@Nonnull byte[] bytes) throws IOException { - return publicKeyRing(new ByteArrayInputStream(bytes)); - } - - public PGPPublicKeyRing publicKeyRing(@Nonnull String asciiArmored) throws IOException { - return publicKeyRing(asciiArmored.getBytes(UTF8)); - } - - public PGPPublicKeyRingCollection publicKeyRingCollection(@Nonnull InputStream inputStream) - throws IOException, PGPException { - return readPublicKeyRingCollection(inputStream); - } - - public PGPPublicKeyRingCollection publicKeyRingCollection(@Nonnull byte[] bytes) throws IOException, PGPException { - return publicKeyRingCollection(new ByteArrayInputStream(bytes)); - } - - public PGPPublicKeyRingCollection publicKeyRingCollection(@Nonnull String asciiArmored) throws IOException, PGPException { - return publicKeyRingCollection(asciiArmored.getBytes(UTF8)); - } - - public PGPSecretKeyRing secretKeyRing(@Nonnull InputStream inputStream) throws IOException { - return readSecretKeyRing(inputStream); - } - - public PGPSecretKeyRing secretKeyRing(@Nonnull byte[] bytes) throws IOException { - return secretKeyRing(new ByteArrayInputStream(bytes)); - } - - public PGPSecretKeyRing secretKeyRing(@Nonnull String asciiArmored) throws IOException { - return secretKeyRing(asciiArmored.getBytes(UTF8)); - } - - public PGPSecretKeyRingCollection secretKeyRingCollection(@Nonnull InputStream inputStream) - throws IOException, PGPException { - return readSecretKeyRingCollection(inputStream); - } - - public PGPSecretKeyRingCollection secretKeyRingCollection(@Nonnull byte[] bytes) throws IOException, PGPException { - return secretKeyRingCollection(new ByteArrayInputStream(bytes)); - } - - public PGPSecretKeyRingCollection secretKeyRingCollection(@Nonnull String asciiArmored) throws IOException, PGPException { - return secretKeyRingCollection(asciiArmored.getBytes(UTF8)); - } - - public PGPKeyRingCollection keyRingCollection(@Nonnull InputStream inputStream, boolean isSilent) - throws IOException, PGPException { - return readKeyRingCollection(inputStream, isSilent); - } - - public PGPKeyRingCollection keyRingCollection(@Nonnull byte[] bytes, boolean isSilent) throws IOException, PGPException { - return keyRingCollection(new ByteArrayInputStream(bytes), isSilent); - } - - public PGPKeyRingCollection keyRingCollection(@Nonnull String asciiArmored, boolean isSilent) throws IOException, PGPException { - return keyRingCollection(asciiArmored.getBytes(UTF8), isSilent); - } - - public static PGPPublicKeyRing readPublicKeyRing(@Nonnull InputStream inputStream) throws IOException { - PGPObjectFactory objectFactory = new PGPObjectFactory( - ArmorUtils.getDecoderStream(inputStream), - ImplementationFactory.getInstance().getKeyFingerprintCalculator()); - Object next; - do { - next = objectFactory.nextObject(); - if (next == null) { - return null; - } - if (next instanceof PGPMarker) { - continue; - } - if (next instanceof PGPPublicKeyRing) { - return (PGPPublicKeyRing) next; - } - } while (true); - } - - public static PGPPublicKeyRingCollection readPublicKeyRingCollection(@Nonnull InputStream inputStream) - throws IOException, PGPException { - PGPObjectFactory objectFactory = new PGPObjectFactory( - ArmorUtils.getDecoderStream(inputStream), - ImplementationFactory.getInstance().getKeyFingerprintCalculator()); - - List rings = new ArrayList<>(); - - Object next; - do { - next = objectFactory.nextObject(); - if (next == null) { - return new PGPPublicKeyRingCollection(rings); - } - if (next instanceof PGPMarker) { - continue; - } - if (next instanceof PGPPublicKeyRing) { - rings.add((PGPPublicKeyRing) next); - } - if (next instanceof PGPPublicKeyRingCollection) { - PGPPublicKeyRingCollection collection = (PGPPublicKeyRingCollection) next; - Iterator iterator = collection.getKeyRings(); - while (iterator.hasNext()) { - rings.add(iterator.next()); - } - } - } while (true); - } - - public static PGPSecretKeyRing readSecretKeyRing(@Nonnull InputStream inputStream) throws IOException { - InputStream decoderStream = ArmorUtils.getDecoderStream(inputStream); - PGPObjectFactory objectFactory = new PGPObjectFactory( - decoderStream, - ImplementationFactory.getInstance().getKeyFingerprintCalculator()); - - Object next; - do { - next = objectFactory.nextObject(); - if (next == null) { - return null; - } - if (next instanceof PGPMarker) { - continue; - } - if (next instanceof PGPSecretKeyRing) { - Streams.drain(decoderStream); - return (PGPSecretKeyRing) next; - } - } while (true); - } - - public static PGPSecretKeyRingCollection readSecretKeyRingCollection(@Nonnull InputStream inputStream) - throws IOException, PGPException { - PGPObjectFactory objectFactory = new PGPObjectFactory( - ArmorUtils.getDecoderStream(inputStream), - ImplementationFactory.getInstance().getKeyFingerprintCalculator()); - - List rings = new ArrayList<>(); - - Object next; - do { - next = objectFactory.nextObject(); - if (next == null) { - return new PGPSecretKeyRingCollection(rings); - } - if (next instanceof PGPMarker) { - continue; - } - if (next instanceof PGPSecretKeyRing) { - rings.add((PGPSecretKeyRing) next); - } - if (next instanceof PGPSecretKeyRingCollection) { - PGPSecretKeyRingCollection collection = (PGPSecretKeyRingCollection) next; - Iterator iterator = collection.getKeyRings(); - while (iterator.hasNext()) { - rings.add(iterator.next()); - } - } - } while (true); - } - - public static PGPKeyRingCollection readKeyRingCollection(@Nonnull InputStream inputStream, boolean isSilent) - throws IOException, PGPException { - return new PGPKeyRingCollection(inputStream, isSilent); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/parsing/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/key/parsing/package-info.java deleted file mode 100644 index 50030499..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/parsing/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Classes related to OpenPGP key reading. - */ -package org.pgpainless.key.parsing; diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/CachingSecretKeyRingProtector.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/CachingSecretKeyRingProtector.java deleted file mode 100644 index 192fb9f9..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/CachingSecretKeyRingProtector.java +++ /dev/null @@ -1,162 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.protection; - -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPKeyRing; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; -import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; -import org.pgpainless.key.OpenPgpV4Fingerprint; -import org.pgpainless.key.protection.passphrase_provider.SecretKeyPassphraseProvider; -import org.pgpainless.util.Passphrase; - -/** - * Implementation of the {@link SecretKeyRingProtector} which holds a map of key ids and their passwords. - * In case the needed passphrase is not contained in the map, the {@code missingPassphraseCallback} will be consulted, - * and the passphrase is added to the map. - */ -public class CachingSecretKeyRingProtector implements SecretKeyRingProtector, SecretKeyPassphraseProvider { - - private final Map cache = new HashMap<>(); - private final SecretKeyRingProtector protector; - private final SecretKeyPassphraseProvider provider; - - public CachingSecretKeyRingProtector() { - this(null); - } - - public CachingSecretKeyRingProtector(@Nullable SecretKeyPassphraseProvider missingPassphraseCallback) { - this( - new HashMap<>(), - KeyRingProtectionSettings.secureDefaultSettings(), - missingPassphraseCallback - ); - } - - public CachingSecretKeyRingProtector(@Nonnull Map passphrases, - @Nonnull KeyRingProtectionSettings protectionSettings, - @Nullable SecretKeyPassphraseProvider missingPassphraseCallback) { - this.cache.putAll(passphrases); - this.protector = new PasswordBasedSecretKeyRingProtector(protectionSettings, this); - this.provider = missingPassphraseCallback; - } - - /** - * Add a passphrase to the cache. - * - * @param keyId id of the key - * @param passphrase passphrase - */ - public void addPassphrase(@Nonnull Long keyId, @Nullable Passphrase passphrase) { - this.cache.put(keyId, passphrase); - } - - /** - * Remember the given passphrase for all keys in the given key ring. - * - * @param keyRing key ring - * @param passphrase passphrase - */ - public void addPassphrase(@Nonnull PGPKeyRing keyRing, @Nullable Passphrase passphrase) { - Iterator keys = keyRing.getPublicKeys(); - while (keys.hasNext()) { - PGPPublicKey publicKey = keys.next(); - addPassphrase(publicKey, passphrase); - } - } - - /** - * Remember the given passphrase for the given (sub-)key. - * - * @param key key - * @param passphrase passphrase - */ - public void addPassphrase(@Nonnull PGPPublicKey key, @Nullable Passphrase passphrase) { - addPassphrase(key.getKeyID(), passphrase); - } - - public void addPassphrase(@Nonnull OpenPgpV4Fingerprint fingerprint, @Nullable Passphrase passphrase) { - addPassphrase(fingerprint.getKeyId(), passphrase); - } - - /** - * Remove a passphrase from the cache. - * The passphrase will be cleared and then removed. - * - * @param keyId id of the key - */ - public void forgetPassphrase(@Nonnull Long keyId) { - Passphrase passphrase = cache.get(keyId); - passphrase.clear(); - cache.remove(keyId); - } - - /** - * Forget the passphrase to all keys in the provided key ring. - * - * @param keyRing key ring - */ - public void forgetPassphrase(@Nonnull PGPKeyRing keyRing) { - Iterator keys = keyRing.getPublicKeys(); - while (keys.hasNext()) { - PGPPublicKey publicKey = keys.next(); - forgetPassphrase(publicKey); - } - } - - /** - * Forget the passphrase of the given public key. - * - * @param key key - */ - public void forgetPassphrase(@Nonnull PGPPublicKey key) { - forgetPassphrase(key.getKeyID()); - } - - @Override - @Nullable - public Passphrase getPassphraseFor(Long keyId) { - Passphrase passphrase = cache.get(keyId); - if (passphrase == null || !passphrase.isValid()) { - if (provider == null) { - return null; - } - passphrase = provider.getPassphraseFor(keyId); - if (passphrase != null) { - cache.put(keyId, passphrase); - } - } - return passphrase; - } - - @Override - public boolean hasPassphrase(Long keyId) { - return cache.containsKey(keyId); - } - - @Override - public boolean hasPassphraseFor(Long keyId) { - return cache.containsKey(keyId); - } - - @Override - @Nullable - public PBESecretKeyDecryptor getDecryptor(@Nonnull Long keyId) throws PGPException { - return protector.getDecryptor(keyId); - } - - @Override - @Nullable - public PBESecretKeyEncryptor getEncryptor(@Nonnull Long keyId) throws PGPException { - return protector.getEncryptor(keyId); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/KeyRingProtectionSettings.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/KeyRingProtectionSettings.java deleted file mode 100644 index ede286c6..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/KeyRingProtectionSettings.java +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.protection; - -import javax.annotation.Nonnull; - -import org.pgpainless.algorithm.HashAlgorithm; -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; - -public class KeyRingProtectionSettings { - - private final SymmetricKeyAlgorithm encryptionAlgorithm; - private final HashAlgorithm hashAlgorithm; - private final int s2kCount; - - public KeyRingProtectionSettings(@Nonnull SymmetricKeyAlgorithm encryptionAlgorithm) { - this(encryptionAlgorithm, HashAlgorithm.SHA1, 0x60); // Same s2kCount as used in BC. - } - - public KeyRingProtectionSettings(@Nonnull SymmetricKeyAlgorithm encryptionAlgorithm, @Nonnull HashAlgorithm hashAlgorithm, int s2kCount) { - this.encryptionAlgorithm = encryptionAlgorithm; - this.hashAlgorithm = hashAlgorithm; - if (s2kCount < 1) { - throw new IllegalArgumentException("s2kCount cannot be less than 1."); - } - this.s2kCount = s2kCount; - } - - public static KeyRingProtectionSettings secureDefaultSettings() { - return new KeyRingProtectionSettings(SymmetricKeyAlgorithm.AES_256); - } - - public @Nonnull SymmetricKeyAlgorithm getEncryptionAlgorithm() { - return encryptionAlgorithm; - } - - public @Nonnull HashAlgorithm getHashAlgorithm() { - return hashAlgorithm; - } - - public int getS2kCount() { - return s2kCount; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/PasswordBasedSecretKeyRingProtector.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/PasswordBasedSecretKeyRingProtector.java deleted file mode 100644 index 2f97863f..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/PasswordBasedSecretKeyRingProtector.java +++ /dev/null @@ -1,110 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.protection; - -import java.util.Iterator; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPKeyRing; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; -import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; -import org.pgpainless.implementation.ImplementationFactory; -import org.pgpainless.key.protection.passphrase_provider.SecretKeyPassphraseProvider; -import org.pgpainless.util.Passphrase; - -/** - * Provides {@link PBESecretKeyDecryptor} and {@link PBESecretKeyEncryptor} objects while getting the passphrases - * from a {@link SecretKeyPassphraseProvider} and using settings from an {@link KeyRingProtectionSettings}. - */ -public class PasswordBasedSecretKeyRingProtector implements SecretKeyRingProtector { - - protected final KeyRingProtectionSettings protectionSettings; - protected final SecretKeyPassphraseProvider passphraseProvider; - - /** - * Constructor. - * Passphrases for keys are sourced from the {@code passphraseProvider} and decryptors/encryptors are constructed - * following the settings given in {@code settings}. - * - * @param settings S2K settings etc. - * @param passphraseProvider provider which provides passphrases. - */ - public PasswordBasedSecretKeyRingProtector(@Nonnull KeyRingProtectionSettings settings, @Nonnull SecretKeyPassphraseProvider passphraseProvider) { - this.protectionSettings = settings; - this.passphraseProvider = passphraseProvider; - } - - public static PasswordBasedSecretKeyRingProtector forKey(PGPKeyRing keyRing, Passphrase passphrase) { - KeyRingProtectionSettings protectionSettings = KeyRingProtectionSettings.secureDefaultSettings(); - SecretKeyPassphraseProvider passphraseProvider = new SecretKeyPassphraseProvider() { - @Override - @Nullable - public Passphrase getPassphraseFor(Long keyId) { - for (Iterator it = keyRing.getPublicKeys(); it.hasNext(); ) { - PGPPublicKey key = it.next(); - if (key.getKeyID() == keyId) { - return passphrase; - } - } - return null; - } - - @Override - public boolean hasPassphrase(Long keyId) { - return keyRing.getPublicKey(keyId) != null; - } - }; - return new PasswordBasedSecretKeyRingProtector(protectionSettings, passphraseProvider); - } - - public static PasswordBasedSecretKeyRingProtector forKey(PGPSecretKey key, Passphrase passphrase) { - KeyRingProtectionSettings protectionSettings = KeyRingProtectionSettings.secureDefaultSettings(); - SecretKeyPassphraseProvider passphraseProvider = new SecretKeyPassphraseProvider() { - @Override - @Nullable - public Passphrase getPassphraseFor(Long keyId) { - if (key.getKeyID() == keyId) { - return passphrase; - } - return null; - } - - @Override - public boolean hasPassphrase(Long keyId) { - return keyId == key.getKeyID(); - } - }; - return new PasswordBasedSecretKeyRingProtector(protectionSettings, passphraseProvider); - } - - @Override - public boolean hasPassphraseFor(Long keyId) { - return passphraseProvider.hasPassphrase(keyId); - } - - @Override - @Nullable - public PBESecretKeyDecryptor getDecryptor(Long keyId) throws PGPException { - Passphrase passphrase = passphraseProvider.getPassphraseFor(keyId); - return passphrase == null ? null : - ImplementationFactory.getInstance().getPBESecretKeyDecryptor(passphrase); - } - - @Override - @Nullable - public PBESecretKeyEncryptor getEncryptor(Long keyId) throws PGPException { - Passphrase passphrase = passphraseProvider.getPassphraseFor(keyId); - return passphrase == null ? null : - ImplementationFactory.getInstance().getPBESecretKeyEncryptor( - protectionSettings.getEncryptionAlgorithm(), - protectionSettings.getHashAlgorithm(), - protectionSettings.getS2kCount(), - passphrase); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/SecretKeyRingProtector.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/SecretKeyRingProtector.java deleted file mode 100644 index 73fb64fc..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/SecretKeyRingProtector.java +++ /dev/null @@ -1,125 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.protection; - -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import javax.annotation.Nullable; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; -import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; -import org.pgpainless.key.protection.passphrase_provider.SecretKeyPassphraseProvider; -import org.pgpainless.util.Passphrase; - -/** - * Task of the {@link SecretKeyRingProtector} is to map encryptor/decryptor objects to key-ids. - * {@link PBESecretKeyEncryptor PBESecretKeyEncryptors}/{@link PBESecretKeyDecryptor PBESecretKeyDecryptors} are used - * to encrypt/decrypt secret keys using a passphrase. - * - * While it is easy to create an implementation of this interface that fits your needs, there are a bunch of - * implementations ready for use. - */ -public interface SecretKeyRingProtector { - - boolean hasPassphraseFor(Long keyId); - - /** - * Return a decryptor for the key of id {@code keyId}. - * This method returns null if the key is unprotected. - * - * @param keyId id of the key - * @return decryptor for the key - */ - @Nullable PBESecretKeyDecryptor getDecryptor(Long keyId) throws PGPException; - - /** - * Return an encryptor for the key of id {@code keyId}. - * This method returns null if the key is unprotected. - * - * @param keyId id of the key - * @return encryptor for the key - * @throws PGPException if the encryptor cannot be created for some reason - */ - @Nullable PBESecretKeyEncryptor getEncryptor(Long keyId) throws PGPException; - - /** - * Return a protector for secret keys. - * The protector maintains an in-memory cache of passphrases and can be extended with new passphrases - * at runtime. - * - * See {@link CachingSecretKeyRingProtector} for how to memorize/forget additional passphrases during runtime. - * - * @param missingPassphraseCallback callback that is used to provide missing passphrases. - * @return caching secret key protector - */ - static CachingSecretKeyRingProtector defaultSecretKeyRingProtector(SecretKeyPassphraseProvider missingPassphraseCallback) { - return new CachingSecretKeyRingProtector( - new HashMap<>(), - KeyRingProtectionSettings.secureDefaultSettings(), - missingPassphraseCallback); - } - - /** - * Use the provided passphrase to lock/unlock all subkeys in the provided key ring. - * - * This protector will use the provided passphrase to lock/unlock all subkeys present in the provided keys object. - * For other keys that are not present in the ring, it will return null. - * - * @param passphrase passphrase - * @param keys key ring - * @return protector - */ - static SecretKeyRingProtector unlockAllKeysWith(Passphrase passphrase, PGPSecretKeyRing keys) { - Map map = new ConcurrentHashMap<>(); - for (PGPSecretKey secretKey : keys) { - map.put(secretKey.getKeyID(), passphrase); - } - return fromPassphraseMap(map); - } - - /** - * Use the provided passphrase to lock/unlock only the provided (sub-)key. - * This protector will only return a non-null encryptor/decryptor based on the provided passphrase if - * {@link #getEncryptor(Long)}/{@link #getDecryptor(Long)} is getting called with the key-id of the provided key. - * - * Otherwise this protector will always return null. - * - * @param passphrase passphrase - * @param key key to lock/unlock - * @return protector - */ - static SecretKeyRingProtector unlockSingleKeyWith(Passphrase passphrase, PGPSecretKey key) { - return PasswordBasedSecretKeyRingProtector.forKey(key, passphrase); - } - - /** - * Protector for unprotected keys. - * This protector returns null for all {@link #getEncryptor(Long)}/{@link #getDecryptor(Long)} calls, - * no matter what the key-id is. - * - * As a consequence, this protector can only "unlock" keys which are not protected using a passphrase, and it will - * leave keys unprotected, should it be used to "protect" a key - * (eg. in {@link org.pgpainless.key.modification.secretkeyring.SecretKeyRingEditor#changePassphraseFromOldPassphrase(Passphrase)}). - * - * @return protector - */ - static SecretKeyRingProtector unprotectedKeys() { - return new UnprotectedKeysProtector(); - } - - /** - * Use the provided map of key-ids and passphrases to unlock keys. - * - * @param passphraseMap map of key ids and their respective passphrases - * @return protector - */ - static SecretKeyRingProtector fromPassphraseMap(Map passphraseMap) { - return new CachingSecretKeyRingProtector(passphraseMap, KeyRingProtectionSettings.secureDefaultSettings(), null); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/UnlockSecretKey.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/UnlockSecretKey.java deleted file mode 100644 index c68e4914..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/UnlockSecretKey.java +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright 2021 Paul Schaub. -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.protection; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPrivateKey; -import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; -import org.pgpainless.exception.WrongPassphraseException; -import org.pgpainless.key.info.KeyInfo; -import org.pgpainless.util.Passphrase; - -public final class UnlockSecretKey { - - private UnlockSecretKey() { - - } - - public static PGPPrivateKey unlockSecretKey(PGPSecretKey secretKey, SecretKeyRingProtector protector) - throws WrongPassphraseException { - try { - PBESecretKeyDecryptor decryptor = null; - if (KeyInfo.isEncrypted(secretKey)) { - decryptor = protector.getDecryptor(secretKey.getKeyID()); - } - return secretKey.extractPrivateKey(decryptor); - } catch (PGPException e) { - throw new WrongPassphraseException(secretKey.getKeyID(), e); - } - } - - public static PGPPrivateKey unlockSecretKey(PGPSecretKey secretKey, PBESecretKeyDecryptor decryptor) throws WrongPassphraseException { - try { - return secretKey.extractPrivateKey(decryptor); - } catch (PGPException e) { - throw new WrongPassphraseException(secretKey.getKeyID(), e); - } - } - - public static PGPPrivateKey unlockSecretKey(PGPSecretKey secretKey, Passphrase passphrase) throws WrongPassphraseException { - return unlockSecretKey(secretKey, SecretKeyRingProtector.unlockSingleKeyWith(passphrase, secretKey)); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/UnprotectedKeysProtector.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/UnprotectedKeysProtector.java deleted file mode 100644 index 26b5c596..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/UnprotectedKeysProtector.java +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.protection; - -import javax.annotation.Nullable; - -import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; -import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; - -/** - * Implementation of the {@link SecretKeyRingProtector} which assumes that all handled keys are not password protected. - */ -public class UnprotectedKeysProtector implements SecretKeyRingProtector { - - @Override - public boolean hasPassphraseFor(Long keyId) { - return true; - } - - @Override - @Nullable - public PBESecretKeyDecryptor getDecryptor(Long keyId) { - return null; - } - - @Override - @Nullable - public PBESecretKeyEncryptor getEncryptor(Long keyId) { - return null; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/fixes/S2KUsageFix.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/fixes/S2KUsageFix.java deleted file mode 100644 index 24fe533a..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/fixes/S2KUsageFix.java +++ /dev/null @@ -1,92 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.protection.fixes; - -import org.bouncycastle.bcpg.SecretKeyPacket; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPrivateKey; -import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; -import org.bouncycastle.openpgp.operator.PGPDigestCalculator; -import org.pgpainless.algorithm.HashAlgorithm; -import org.pgpainless.exception.WrongPassphraseException; -import org.pgpainless.implementation.ImplementationFactory; -import org.pgpainless.key.protection.SecretKeyRingProtector; -import org.pgpainless.key.protection.UnlockSecretKey; - -/** - * Repair class to fix keys which use S2K usage of value {@link SecretKeyPacket#USAGE_CHECKSUM}. - * The method {@link #replaceUsageChecksumWithUsageSha1(PGPSecretKeyRing, SecretKeyRingProtector)} ensures - * that such keys are encrypted using S2K usage {@link SecretKeyPacket#USAGE_SHA1} instead. - * - * @see Related PGPainless Bug Report - * @see Related PGPainless Feature Request - * @see Related upstream BC bug report - */ -public final class S2KUsageFix { - - private S2KUsageFix() { - - } - - /** - * Repair method for keys which use S2K usage
USAGE_CHECKSUM
which is deemed insecure. - * This method fixes the private keys by changing them to
USAGE_SHA1
instead. - * - * @param keys keys - * @param protector protector to unlock and re-lock affected private keys - * @return fixed key ring - * @throws PGPException in case of a PGP error. - */ - public static PGPSecretKeyRing replaceUsageChecksumWithUsageSha1(PGPSecretKeyRing keys, SecretKeyRingProtector protector) throws PGPException { - return replaceUsageChecksumWithUsageSha1(keys, protector, false); - } - - /** - * Repair method for keys which use S2K usage
USAGE_CHECKSUM
which is deemed insecure. - * This method fixes the private keys by changing them to
USAGE_SHA1
instead. - * - * @param keys keys - * @param protector protector to unlock and re-lock affected private keys - * @param skipKeysWithMissingPassphrase if set to true, missing subkey passphrases will cause the subkey to stay unaffected. - * @return fixed key ring - * @throws PGPException in case of a PGP error. - */ - public static PGPSecretKeyRing replaceUsageChecksumWithUsageSha1(PGPSecretKeyRing keys, - SecretKeyRingProtector protector, - boolean skipKeysWithMissingPassphrase) throws PGPException { - PGPDigestCalculator digestCalculator = ImplementationFactory.getInstance().getPGPDigestCalculator(HashAlgorithm.SHA1); - for (PGPSecretKey key : keys) { - // CHECKSUM is not recommended - if (key.getS2KUsage() != SecretKeyPacket.USAGE_CHECKSUM) { - continue; - } - - long keyId = key.getKeyID(); - PBESecretKeyEncryptor encryptor = protector.getEncryptor(keyId); - if (encryptor == null) { - if (skipKeysWithMissingPassphrase) { - continue; - } - throw new WrongPassphraseException("Missing passphrase for key with ID " + Long.toHexString(keyId)); - } - - PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(key, protector); - // This constructor makes use of USAGE_SHA1 by default - PGPSecretKey fixedKey = new PGPSecretKey( - privateKey, - key.getPublicKey(), - digestCalculator, - key.isMasterKey(), - protector.getEncryptor(keyId) - ); - - // replace the original key with the fixed one - keys = PGPSecretKeyRing.insertSecretKey(keys, fixedKey); - } - return keys; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/fixes/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/fixes/package-info.java deleted file mode 100644 index 06c299b1..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/fixes/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Secret Key Protection Fixes. - */ -package org.pgpainless.key.protection.fixes; diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/package-info.java deleted file mode 100644 index b936025f..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Classes related to OpenPGP secret key password protection. - */ -package org.pgpainless.key.protection; diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/MapBasedPassphraseProvider.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/MapBasedPassphraseProvider.java deleted file mode 100644 index 859758b1..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/MapBasedPassphraseProvider.java +++ /dev/null @@ -1,42 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.protection.passphrase_provider; - -import java.util.Map; -import javax.annotation.Nullable; - -import org.pgpainless.util.Passphrase; - -/** - * Implementation of the {@link SecretKeyPassphraseProvider} that holds a map of different {@link Passphrase passphrases}. - * It will return the right passphrase depending on the key-id. - * - * Note: This provider might return null! - * TODO: Make this null-safe and throw an exception instead? - */ -public class MapBasedPassphraseProvider implements SecretKeyPassphraseProvider { - - private final Map map; - - /** - * Create a new map based passphrase provider. - * - * @param passphraseMap map of key-ids and passphrases - */ - public MapBasedPassphraseProvider(Map passphraseMap) { - this.map = passphraseMap; - } - - @Nullable - @Override - public Passphrase getPassphraseFor(Long keyId) { - return map.get(keyId); - } - - @Override - public boolean hasPassphrase(Long keyId) { - return map.containsKey(keyId); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/SecretKeyPassphraseProvider.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/SecretKeyPassphraseProvider.java deleted file mode 100644 index 59cb39ce..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/SecretKeyPassphraseProvider.java +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.protection.passphrase_provider; - -import javax.annotation.Nullable; - -import org.bouncycastle.openpgp.PGPSecretKey; -import org.pgpainless.util.Passphrase; - -/** - * Interface to allow the user to provide a {@link Passphrase} for an encrypted OpenPGP secret key. - */ -public interface SecretKeyPassphraseProvider { - - /** - * Return a passphrase for the given secret key. - * If no record is found, return null. - * Note: In case of an unprotected secret key, this method must may not return null, but a {@link Passphrase} with - * a content of null. - * - * @param secretKey secret key - * @return passphrase or null, if no passphrase record is found. - */ - @Nullable default Passphrase getPassphraseFor(PGPSecretKey secretKey) { - return getPassphraseFor(secretKey.getKeyID()); - } - /** - * Return a passphrase for the given key. If no record has been found, return null. - * Note: In case of an unprotected secret key, this method must may not return null, but a {@link Passphrase} with - * a content of null. - * - * @param keyId if of the secret key - * @return passphrase or null, if no passphrase record has been found. - */ - @Nullable Passphrase getPassphraseFor(Long keyId); - - boolean hasPassphrase(Long keyId); -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/SolitaryPassphraseProvider.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/SolitaryPassphraseProvider.java deleted file mode 100644 index b439ef56..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/SolitaryPassphraseProvider.java +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.protection.passphrase_provider; - -import javax.annotation.Nullable; - -import org.pgpainless.util.Passphrase; - -/** - * Implementation of the {@link SecretKeyPassphraseProvider} that holds a single {@link Passphrase}. - */ -public class SolitaryPassphraseProvider implements SecretKeyPassphraseProvider { - - private final Passphrase passphrase; - - public SolitaryPassphraseProvider(Passphrase passphrase) { - this.passphrase = passphrase; - } - - @Nullable - @Override - public Passphrase getPassphraseFor(Long keyId) { - // always return the same passphrase. - return passphrase; - } - - @Override - public boolean hasPassphrase(Long keyId) { - return true; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/package-info.java deleted file mode 100644 index e70ad81f..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Passphrase Provider classes. - */ -package org.pgpainless.key.protection.passphrase_provider; diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyIdUtil.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyIdUtil.java deleted file mode 100644 index e6ab4f48..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyIdUtil.java +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.util; - -import java.math.BigInteger; -import java.util.regex.Pattern; - -public final class KeyIdUtil { - - private KeyIdUtil() { - - } - - private static final Pattern LONG_KEY_ID = Pattern.compile("^[0-9A-Fa-f]{16}$"); - - /** - * Convert a long key-id into a key-id. - * A long key-id is a 16 digit hex string. - * - * @param longKeyId 16-digit hexadecimal string - * @return key-id converted to {@link Long}. - */ - public static long fromLongKeyId(String longKeyId) { - if (!LONG_KEY_ID.matcher(longKeyId).matches()) { - throw new IllegalArgumentException("Provided long key-id does not match expected format. " + - "A long key-id consists of 16 hexadecimal characters."); - } - - return new BigInteger(longKeyId, 16).longValue(); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java deleted file mode 100644 index a85ea446..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java +++ /dev/null @@ -1,157 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.util; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Iterator; -import java.util.List; -import java.util.NoSuchElementException; -import javax.annotation.Nonnull; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPKeyRing; -import org.bouncycastle.openpgp.PGPPrivateKey; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; -import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; -import org.pgpainless.key.protection.SecretKeyRingProtector; -import org.pgpainless.key.protection.UnlockSecretKey; - -public final class KeyRingUtils { - - private KeyRingUtils() { - - } - - /** - * Return the primary {@link PGPSecretKey} from the provided {@link PGPSecretKeyRing}. - * If it has no primary secret key, throw a {@link NoSuchElementException}. - * - * @param secretKeys secret keys - * @return primary secret key - */ - public static PGPSecretKey requirePrimarySecretKeyFrom(PGPSecretKeyRing secretKeys) { - PGPSecretKey primarySecretKey = getPrimarySecretKeyFrom(secretKeys); - if (primarySecretKey == null) { - throw new NoSuchElementException("Provided PGPSecretKeyRing has no primary secret key."); - } - return primarySecretKey; - } - - /** - * Return the primary {@link PGPSecretKey} from the provided {@link PGPSecretKeyRing} or null if it has none. - * - * @param secretKeys secret key ring - * @return primary secret key - */ - public static PGPSecretKey getPrimarySecretKeyFrom(PGPSecretKeyRing secretKeys) { - PGPSecretKey secretKey = secretKeys.getSecretKey(); - if (secretKey.isMasterKey()) { - return secretKey; - } - return null; - } - - /** - * Return the primary {@link PGPPublicKey} from the provided key ring. - * Throws a {@link NoSuchElementException} if the key ring has no primary public key. - * - * @param keyRing key ring - * @return primary public key - */ - public static PGPPublicKey requirePrimaryPublicKeyFrom(PGPKeyRing keyRing) { - PGPPublicKey primaryPublicKey = getPrimaryPublicKeyFrom(keyRing); - if (primaryPublicKey == null) { - throw new NoSuchElementException("Provided PGPKeyRing has no primary public key."); - } - return primaryPublicKey; - } - - /** - * Return the primary {@link PGPPublicKey} from the provided key ring or null if it has none. - * - * @param keyRing key ring - * @return primary public key - */ - public static PGPPublicKey getPrimaryPublicKeyFrom(PGPKeyRing keyRing) { - PGPPublicKey primaryPublicKey = keyRing.getPublicKey(); - if (primaryPublicKey.isMasterKey()) { - return primaryPublicKey; - } - return null; - } - - public static PGPPublicKey getPublicKeyFrom(PGPKeyRing keyRing, long subKeyId) { - return keyRing.getPublicKey(subKeyId); - } - - public static PGPPublicKey requirePublicKeyFrom(PGPKeyRing keyRing, long subKeyId) { - PGPPublicKey publicKey = getPublicKeyFrom(keyRing, subKeyId); - if (publicKey == null) { - throw new IllegalArgumentException("KeyRing does not contain public key with keyID " + Long.toHexString(subKeyId)); - } - return publicKey; - } - - public static PGPSecretKey requireSecretKeyFrom(PGPSecretKeyRing keyRing, long subKeyId) { - PGPSecretKey secretKey = keyRing.getSecretKey(subKeyId); - if (secretKey == null) { - throw new IllegalArgumentException("KeyRing does not contain secret key with keyID " + Long.toHexString(subKeyId)); - } - return secretKey; - } - - /** - * Extract a {@link PGPPublicKeyRing} containing all public keys from the provided {@link PGPSecretKeyRing}. - * - * @param secretKeys secret key ring - * @return public key ring - */ - public static PGPPublicKeyRing publicKeyRingFrom(PGPSecretKeyRing secretKeys) { - List publicKeyList = new ArrayList<>(); - Iterator publicKeyIterator = secretKeys.getPublicKeys(); - while (publicKeyIterator.hasNext()) { - publicKeyList.add(publicKeyIterator.next()); - } - PGPPublicKeyRing publicKeyRing = new PGPPublicKeyRing(publicKeyList); - return publicKeyRing; - } - - /** - * Unlock a {@link PGPSecretKey} and return the resulting {@link PGPPrivateKey}. - * - * @param secretKey secret key - * @param protector protector to unlock the secret key - * @return private key - * - * @throws PGPException if something goes wrong (eg. wrong passphrase) - */ - public static PGPPrivateKey unlockSecretKey(PGPSecretKey secretKey, SecretKeyRingProtector protector) throws PGPException { - return UnlockSecretKey.unlockSecretKey(secretKey, protector); - } - - /* - PGPXxxKeyRing -> PGPXxxKeyRingCollection - */ - public static PGPPublicKeyRingCollection keyRingsToKeyRingCollection(@Nonnull PGPPublicKeyRing... rings) - throws IOException, PGPException { - return new PGPPublicKeyRingCollection(Arrays.asList(rings)); - } - - public static PGPSecretKeyRingCollection keyRingsToKeyRingCollection(@Nonnull PGPSecretKeyRing... rings) - throws IOException, PGPException { - return new PGPSecretKeyRingCollection(Arrays.asList(rings)); - } - - public static boolean keyRingContainsKeyWithId(@Nonnull PGPPublicKeyRing ring, - long keyId) { - return ring.getPublicKey(keyId) != null; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/OpenPgpKeyAttributeUtil.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/OpenPgpKeyAttributeUtil.java index 673b86ec..f7a78404 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/OpenPgpKeyAttributeUtil.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/OpenPgpKeyAttributeUtil.java @@ -8,7 +8,9 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.Iterator; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPSignature; @@ -23,7 +25,6 @@ public final class OpenPgpKeyAttributeUtil { public static List getPreferredHashAlgorithms(PGPPublicKey publicKey) { List hashAlgorithms = new ArrayList<>(); - // TODO: I'd assume that we have to use publicKey.getKeySignatures() here, but that is empty... Iterator keySignatures = publicKey.getSignatures(); while (keySignatures.hasNext()) { PGPSignature signature = (PGPSignature) keySignatures.next(); @@ -33,7 +34,11 @@ public final class OpenPgpKeyAttributeUtil { continue; } - SignatureType signatureType = SignatureType.valueOf(signature.getSignatureType()); + SignatureType signatureType = SignatureType.fromCode(signature.getSignatureType()); + if (signatureType == null) { + // unknown signature type + continue; + } if (signatureType == SignatureType.POSITIVE_CERTIFICATION || signatureType == SignatureType.GENERIC_CERTIFICATION) { int[] hashAlgos = signature.getHashedSubPackets().getPreferredHashAlgorithms(); @@ -41,11 +46,12 @@ public final class OpenPgpKeyAttributeUtil { continue; } for (int h : hashAlgos) { - hashAlgorithms.add(HashAlgorithm.fromId(h)); + HashAlgorithm algorithm = HashAlgorithm.fromId(h); + if (algorithm != null) { + hashAlgorithms.add(algorithm); + } } // Exit the loop after the first key signature with hash algorithms. - // TODO: Find out, if it is possible that there are multiple key signatures which specify preferred - // algorithms and how to deal with that. break; } } @@ -69,8 +75,8 @@ public final class OpenPgpKeyAttributeUtil { continue; } - SignatureType signatureType = SignatureType.valueOf(signature.getSignatureType()); - if (signatureType != SignatureType.POSITIVE_CERTIFICATION + SignatureType signatureType = SignatureType.fromCode(signature.getSignatureType()); + if (signatureType == null || signatureType != SignatureType.POSITIVE_CERTIFICATION && signatureType != SignatureType.GENERIC_CERTIFICATION) { continue; } @@ -87,4 +93,21 @@ public final class OpenPgpKeyAttributeUtil { } return Collections.singletonList(hashAlgorithm); } + + /** + * Try to extract hash algorithm preferences from self signatures. + * If no self-signature containing hash algorithm preferences is found, + * try to derive a hash algorithm preference by inspecting the hash algorithm used by existing + * self-signatures. + * + * @param publicKey key + * @return hash algorithm preferences (might be empty!) + */ + public static Set getOrGuessPreferredHashAlgorithms(PGPPublicKey publicKey) { + List preferredHashAlgorithms = OpenPgpKeyAttributeUtil.getPreferredHashAlgorithms(publicKey); + if (preferredHashAlgorithms.isEmpty()) { + preferredHashAlgorithms = OpenPgpKeyAttributeUtil.guessPreferredHashAlgorithms(publicKey); + } + return new LinkedHashSet<>(preferredHashAlgorithms); + } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/RevocationAttributes.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/RevocationAttributes.java deleted file mode 100644 index fba54559..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/RevocationAttributes.java +++ /dev/null @@ -1,127 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.util; - -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -public final class RevocationAttributes { - - public enum Reason { - NO_REASON((byte) 0), - KEY_SUPERSEDED((byte) 1), - KEY_COMPROMISED((byte) 2), - KEY_RETIRED((byte) 3), - USER_ID_NO_LONGER_VALID((byte) 32), - ; - - private static final Map MAP = new ConcurrentHashMap<>(); - static { - for (Reason r : Reason.values()) { - MAP.put(r.reasonCode, r); - } - } - - public static Reason fromCode(byte code) { - Reason reason = MAP.get(code); - if (reason == null) { - throw new IllegalArgumentException("Invalid revocation reason: " + code); - } - return reason; - } - - public static boolean isHardRevocation(byte code) { - Reason reason = MAP.get(code); - return reason != KEY_SUPERSEDED && reason != KEY_RETIRED && reason != USER_ID_NO_LONGER_VALID; - } - - private final byte reasonCode; - - Reason(byte reasonCode) { - this.reasonCode = reasonCode; - } - - public byte code() { - return reasonCode; - } - - @Override - public String toString() { - return code() + " - " + name(); - } - } - - public enum RevocationType { - KEY_REVOCATION, - CERT_REVOCATION - } - - private final Reason reason; - private final String description; - - private RevocationAttributes(Reason reason, String description) { - this.reason = reason; - this.description = description; - } - - public Reason getReason() { - return reason; - } - - public String getDescription() { - return description; - } - - public static WithReason createKeyRevocation() { - return new WithReason(RevocationType.KEY_REVOCATION); - } - - public static WithReason createCertificateRevocation() { - return new WithReason(RevocationType.CERT_REVOCATION); - } - - public static final class WithReason { - - private final RevocationType type; - - private WithReason(RevocationType type) { - this.type = type; - } - - public WithDescription withReason(Reason reason) { - throwIfReasonTypeMismatch(reason, type); - return new WithDescription(reason); - } - - private void throwIfReasonTypeMismatch(Reason reason, RevocationType type) { - if (type == RevocationType.KEY_REVOCATION) { - if (reason == Reason.USER_ID_NO_LONGER_VALID) { - throw new IllegalArgumentException("Reason " + reason + " can only be used for certificate revocations, not to revoke keys."); - } - } else if (type == RevocationType.CERT_REVOCATION) { - switch (reason) { - case KEY_SUPERSEDED: - case KEY_COMPROMISED: - case KEY_RETIRED: - throw new IllegalArgumentException("Reason " + reason + " can only be used for key revocations, not to revoke certificates."); - } - } - } - - } - - public static final class WithDescription { - - private final Reason reason; - - private WithDescription(Reason reason) { - this.reason = reason; - } - - public RevocationAttributes withDescription(String description) { - return new RevocationAttributes(reason, description); - } - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java deleted file mode 100644 index 3cd0a444..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java +++ /dev/null @@ -1,184 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub , 2021 Flowcrypt a.s. -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.util; - -import javax.annotation.Nonnull; - -public final class UserId implements CharSequence { - public static final class Builder { - private String name; - private String comment; - private String email; - - private Builder() { - } - - private Builder(String name, String comment, String email) { - this.name = name; - this.comment = comment; - this.email = email; - } - - public Builder withName(String name) { - checkNotNull("name", name); - this.name = name; - return this; - } - - public Builder withComment(String comment) { - checkNotNull("comment", comment); - this.comment = comment; - return this; - } - - public Builder withEmail(String email) { - checkNotNull("email", email); - this.email = email; - return this; - } - - public Builder noName() { - name = null; - return this; - } - - public Builder noComment() { - comment = null; - return this; - } - - public Builder noEmail() { - email = null; - return this; - } - - public UserId build() { - return new UserId(name, comment, email); - } - } - - private final String name; - private final String comment; - private final String email; - private long hash = Long.MAX_VALUE; - - private UserId(String name, String comment, String email) { - this.name = name; - this.comment = comment; - this.email = email; - } - - public static UserId onlyEmail(String email) { - checkNotNull("email", email); - return new UserId(null, null, email); - } - - public static UserId nameAndEmail(String name, String email) { - checkNotNull("name", name); - checkNotNull("email", email); - return new UserId(name, null, email); - } - - public static Builder newBuilder() { - return new Builder(); - } - - public Builder toBuilder() { - return new Builder(name, comment, email); - } - - public String getName() { - return name; - } - - public String getComment() { - return comment; - } - - public String getEmail() { - return email; - } - - @Override - public int length() { - return toString().length(); - } - - @Override - public char charAt(int i) { - return toString().charAt(i); - } - - @Override - public CharSequence subSequence(int i, int i1) { - return toString().subSequence(i, i1); - } - - @Override - public @Nonnull String toString() { - return asString(false); - } - - /** - * Returns a string representation of the object. - * @param ignoreEmptyValues Flag which indicates that empty string values should not be outputted. - * @return a string representation of the object. - */ - public String asString(boolean ignoreEmptyValues) { - StringBuilder sb = new StringBuilder(); - if (name != null && (!ignoreEmptyValues || !name.isEmpty())) { - sb.append(name); - } - if (comment != null && (!ignoreEmptyValues || !comment.isEmpty())) { - sb.append(" (").append(comment).append(')'); - } - if (email != null && (!ignoreEmptyValues || !email.isEmpty())) { - final boolean moreThanJustEmail = sb.length() > 0; - if (moreThanJustEmail) sb.append(" <"); - sb.append(email); - if (moreThanJustEmail) sb.append('>'); - } - return sb.toString(); - } - - @Override - public boolean equals(Object o) { - if (o == null) return false; - if (o == this) return true; - if (!(o instanceof UserId)) return false; - final UserId other = (UserId) o; - return isEqualComponent(name, other.name, false) - && isEqualComponent(comment, other.comment, false) - && isEqualComponent(email, other.email, true); - } - - @Override - public int hashCode() { - if (hash != Long.MAX_VALUE) { - return (int) hash; - } else { - int hashCode = 7; - hashCode = 31 * hashCode + (name == null ? 0 : name.hashCode()); - hashCode = 31 * hashCode + (comment == null ? 0 : comment.hashCode()); - hashCode = 31 * hashCode + (email == null ? 0 : email.toLowerCase().hashCode()); - this.hash = hashCode; - return hashCode; - } - } - - private static boolean isEqualComponent(String value, String otherValue, boolean ignoreCase) { - final boolean valueIsNull = (value == null); - final boolean otherValueIsNull = (otherValue == null); - return (valueIsNull && otherValueIsNull) - || (!valueIsNull && !otherValueIsNull - && (ignoreCase ? value.equalsIgnoreCase(otherValue) : value.equals(otherValue))); - } - - private static void checkNotNull(String paramName, String value) { - if (value == null) { - throw new IllegalArgumentException(paramName + " must be not null"); - } - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/policy/Policy.java b/pgpainless-core/src/main/java/org/pgpainless/policy/Policy.java deleted file mode 100644 index 506f245c..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/policy/Policy.java +++ /dev/null @@ -1,473 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.policy; - -import java.util.Arrays; -import java.util.Collections; -import java.util.EnumMap; -import java.util.List; -import java.util.Map; - -import javax.annotation.Nonnull; - -import org.pgpainless.algorithm.AlgorithmSuite; -import org.pgpainless.algorithm.CompressionAlgorithm; -import org.pgpainless.algorithm.HashAlgorithm; -import org.pgpainless.algorithm.PublicKeyAlgorithm; -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; -import org.pgpainless.util.NotationRegistry; - -/** - * Policy class used to configure acceptable algorithm suites etc. - */ -public final class Policy { - - private static Policy INSTANCE; - - private HashAlgorithmPolicy signatureHashAlgorithmPolicy = - HashAlgorithmPolicy.defaultSignatureAlgorithmPolicy(); - private HashAlgorithmPolicy revocationSignatureHashAlgorithmPolicy = - HashAlgorithmPolicy.defaultRevocationSignatureHashAlgorithmPolicy(); - private SymmetricKeyAlgorithmPolicy symmetricKeyEncryptionAlgorithmPolicy = - SymmetricKeyAlgorithmPolicy.defaultSymmetricKeyEncryptionAlgorithmPolicy(); - private SymmetricKeyAlgorithmPolicy symmetricKeyDecryptionAlgorithmPolicy = - SymmetricKeyAlgorithmPolicy.defaultSymmetricKeyDecryptionAlgorithmPolicy(); - private CompressionAlgorithmPolicy compressionAlgorithmPolicy = - CompressionAlgorithmPolicy.defaultCompressionAlgorithmPolicy(); - private PublicKeyAlgorithmPolicy publicKeyAlgorithmPolicy = - PublicKeyAlgorithmPolicy.defaultPublicKeyAlgorithmPolicy(); - private final NotationRegistry notationRegistry = new NotationRegistry(); - - private AlgorithmSuite keyGenerationAlgorithmSuite = AlgorithmSuite.getDefaultAlgorithmSuite(); - - Policy() { - } - - /** - * Return the singleton instance of PGPainless' policy. - * - * @return singleton instance - */ - public static Policy getInstance() { - if (INSTANCE == null) { - INSTANCE = new Policy(); - } - return INSTANCE; - } - - /** - * Return the hash algorithm policy for signatures. - * @return hash algorithm policy - */ - public HashAlgorithmPolicy getSignatureHashAlgorithmPolicy() { - return signatureHashAlgorithmPolicy; - } - - /** - * Set a custom hash algorithm policy for signatures. - * - * @param policy custom policy - */ - public void setSignatureHashAlgorithmPolicy(HashAlgorithmPolicy policy) { - if (policy == null) { - throw new NullPointerException("Policy cannot be null."); - } - this.signatureHashAlgorithmPolicy = policy; - } - - /** - * Return the hash algorithm policy for revocations. - * This policy is separate from {@link #getSignatureHashAlgorithmPolicy()}, as PGPainless by default uses a - * less strict policy when it comes to acceptable algorithms. - * - * @return revocation signature hash algorithm policy - */ - public HashAlgorithmPolicy getRevocationSignatureHashAlgorithmPolicy() { - return revocationSignatureHashAlgorithmPolicy; - } - - /** - * Set a custom hash algorithm policy for revocations. - * - * @param policy custom policy - */ - public void setRevocationSignatureHashAlgorithmPolicy(HashAlgorithmPolicy policy) { - if (policy == null) { - throw new NullPointerException("Policy cannot be null."); - } - this.revocationSignatureHashAlgorithmPolicy = policy; - } - - /** - * Return the symmetric encryption algorithm policy for encryption. - * This policy defines which symmetric algorithms are acceptable when producing encrypted messages. - * - * @return symmetric algorithm policy for encryption - */ - public SymmetricKeyAlgorithmPolicy getSymmetricKeyEncryptionAlgorithmPolicy() { - return symmetricKeyEncryptionAlgorithmPolicy; - } - - /** - * Return the symmetric encryption algorithm policy for decryption. - * This policy defines which symmetric algorithms are acceptable when decrypting encrypted messages. - * - * @return symmetric algorithm policy for decryption - */ - public SymmetricKeyAlgorithmPolicy getSymmetricKeyDecryptionAlgorithmPolicy() { - return symmetricKeyDecryptionAlgorithmPolicy; - } - - /** - * Set a custom symmetric encryption algorithm policy for encrypting messages. - * - * @param policy custom policy - */ - public void setSymmetricKeyEncryptionAlgorithmPolicy(SymmetricKeyAlgorithmPolicy policy) { - if (policy == null) { - throw new NullPointerException("Policy cannot be null."); - } - this.symmetricKeyEncryptionAlgorithmPolicy = policy; - } - - /** - * Set a custom symmetric encryption algorithm policy for decrypting messages. - * - * @param policy custom policy - */ - public void setSymmetricKeyDecryptionAlgorithmPolicy(SymmetricKeyAlgorithmPolicy policy) { - if (policy == null) { - throw new NullPointerException("Policy cannot be null."); - } - this.symmetricKeyDecryptionAlgorithmPolicy = policy; - } - - public CompressionAlgorithmPolicy getCompressionAlgorithmPolicy() { - return compressionAlgorithmPolicy; - } - - public void setCompressionAlgorithmPolicy(CompressionAlgorithmPolicy policy) { - if (policy == null) { - throw new NullPointerException("Compression policy cannot be null."); - } - this.compressionAlgorithmPolicy = policy; - } - - /** - * Return the current public key algorithm policy. - * - * @return public key algorithm policy - */ - public PublicKeyAlgorithmPolicy getPublicKeyAlgorithmPolicy() { - return publicKeyAlgorithmPolicy; - } - - /** - * Set a custom public key algorithm policy. - * - * @param publicKeyAlgorithmPolicy custom policy - */ - public void setPublicKeyAlgorithmPolicy(PublicKeyAlgorithmPolicy publicKeyAlgorithmPolicy) { - if (publicKeyAlgorithmPolicy == null) { - throw new NullPointerException("Public key algorithm policy cannot be null."); - } - this.publicKeyAlgorithmPolicy = publicKeyAlgorithmPolicy; - } - - public static final class SymmetricKeyAlgorithmPolicy { - - private final SymmetricKeyAlgorithm defaultSymmetricKeyAlgorithm; - private final List acceptableSymmetricKeyAlgorithms; - - public SymmetricKeyAlgorithmPolicy(SymmetricKeyAlgorithm defaultSymmetricKeyAlgorithm, List acceptableSymmetricKeyAlgorithms) { - this.defaultSymmetricKeyAlgorithm = defaultSymmetricKeyAlgorithm; - this.acceptableSymmetricKeyAlgorithms = Collections.unmodifiableList(acceptableSymmetricKeyAlgorithms); - } - - /** - * Return the default symmetric key algorithm. - * This algorithm is used as a fallback when no consensus about symmetric algorithms can be reached. - * - * @return default symmetric encryption algorithm - */ - public SymmetricKeyAlgorithm getDefaultSymmetricKeyAlgorithm() { - return defaultSymmetricKeyAlgorithm; - } - - /** - * Return true if the given symmetric encryption algorithm is acceptable by this policy. - * - * @param algorithm algorithm - * @return true if algorithm is acceptable, false otherwise - */ - public boolean isAcceptable(SymmetricKeyAlgorithm algorithm) { - return acceptableSymmetricKeyAlgorithms.contains(algorithm); - } - - /** - * Return true if the given symmetric encryption algorithm is acceptable by this policy. - * - * @param algorithmId algorithm - * @return true if algorithm is acceptable, false otherwise - */ - public boolean isAcceptable(int algorithmId) { - SymmetricKeyAlgorithm algorithm = SymmetricKeyAlgorithm.fromId(algorithmId); - return isAcceptable(algorithm); - } - - /** - * The default symmetric encryption algorithm policy of PGPainless. - * - * @return default symmetric encryption algorithm policy - */ - public static SymmetricKeyAlgorithmPolicy defaultSymmetricKeyEncryptionAlgorithmPolicy() { - return new SymmetricKeyAlgorithmPolicy(SymmetricKeyAlgorithm.AES_256, Arrays.asList( - // Reject: Unencrypted, IDEA, TripleDES, CAST5 - SymmetricKeyAlgorithm.AES_256, - SymmetricKeyAlgorithm.AES_192, - SymmetricKeyAlgorithm.AES_128, - SymmetricKeyAlgorithm.BLOWFISH, - SymmetricKeyAlgorithm.TWOFISH, - SymmetricKeyAlgorithm.CAMELLIA_256, - SymmetricKeyAlgorithm.CAMELLIA_192, - SymmetricKeyAlgorithm.CAMELLIA_128 - )); - } - - /** - * The default symmetric decryption algorithm policy of PGPainless. - * - * @return default symmetric decryption algorithm policy - */ - public static SymmetricKeyAlgorithmPolicy defaultSymmetricKeyDecryptionAlgorithmPolicy() { - return new SymmetricKeyAlgorithmPolicy(SymmetricKeyAlgorithm.AES_256, Arrays.asList( - // Reject: Unencrypted, IDEA, TripleDES - SymmetricKeyAlgorithm.CAST5, - SymmetricKeyAlgorithm.AES_256, - SymmetricKeyAlgorithm.AES_192, - SymmetricKeyAlgorithm.AES_128, - SymmetricKeyAlgorithm.BLOWFISH, - SymmetricKeyAlgorithm.TWOFISH, - SymmetricKeyAlgorithm.CAMELLIA_256, - SymmetricKeyAlgorithm.CAMELLIA_192, - SymmetricKeyAlgorithm.CAMELLIA_128 - )); - } - - /** - * Select the best acceptable algorithm from the options list. - * The best algorithm is the first algorithm we encounter in our list of acceptable algorithms that - * is also contained in the list of options. - * - * - * @param options list of algorithm options - * @return best - */ - public SymmetricKeyAlgorithm selectBest(List options) { - for (SymmetricKeyAlgorithm acceptable : acceptableSymmetricKeyAlgorithms) { - if (options.contains(acceptable)) { - return acceptable; - } - } - return null; - } - } - - public static final class HashAlgorithmPolicy { - - private final HashAlgorithm defaultHashAlgorithm; - private final List acceptableHashAlgorithms; - - public HashAlgorithmPolicy(HashAlgorithm defaultHashAlgorithm, List acceptableHashAlgorithms) { - this.defaultHashAlgorithm = defaultHashAlgorithm; - this.acceptableHashAlgorithms = Collections.unmodifiableList(acceptableHashAlgorithms); - } - - /** - * Return the default hash algorithm. - * This algorithm is used as a fallback when no consensus about hash algorithms can be reached. - * - * @return default hash algorithm - */ - public HashAlgorithm defaultHashAlgorithm() { - return defaultHashAlgorithm; - } - - /** - * Return true if the the given hash algorithm is acceptable by this policy. - * - * @param hashAlgorithm hash algorithm - * @return true if the hash algorithm is acceptable, false otherwise - */ - public boolean isAcceptable(HashAlgorithm hashAlgorithm) { - return acceptableHashAlgorithms.contains(hashAlgorithm); - } - - /** - * Return true if the the given hash algorithm is acceptable by this policy. - * - * @param algorithmId hash algorithm - * @return true if the hash algorithm is acceptable, false otherwise - */ - public boolean isAcceptable(int algorithmId) { - HashAlgorithm algorithm = HashAlgorithm.fromId(algorithmId); - return isAcceptable(algorithm); - } - - /** - * The default signature hash algorithm policy of PGPainless. - * Note that this policy is only used for non-revocation signatures. - * For revocation signatures {@link #defaultRevocationSignatureHashAlgorithmPolicy()} is used instead. - * - * @return default signature hash algorithm policy - */ - public static HashAlgorithmPolicy defaultSignatureAlgorithmPolicy() { - return new HashAlgorithmPolicy(HashAlgorithm.SHA512, Arrays.asList( - HashAlgorithm.SHA224, - HashAlgorithm.SHA256, - HashAlgorithm.SHA384, - HashAlgorithm.SHA512 - )); - } - - /** - * The default revocation signature hash algorithm policy of PGPainless. - * - * @return default revocation signature hash algorithm policy - */ - public static HashAlgorithmPolicy defaultRevocationSignatureHashAlgorithmPolicy() { - return new HashAlgorithmPolicy(HashAlgorithm.SHA512, Arrays.asList( - HashAlgorithm.RIPEMD160, - HashAlgorithm.SHA1, - HashAlgorithm.SHA224, - HashAlgorithm.SHA256, - HashAlgorithm.SHA384, - HashAlgorithm.SHA512 - )); - } - } - - public static final class CompressionAlgorithmPolicy { - - private final CompressionAlgorithm defaultCompressionAlgorithm; - private final List acceptableCompressionAlgorithms; - - public CompressionAlgorithmPolicy(CompressionAlgorithm defaultCompressionAlgorithm, - List acceptableCompressionAlgorithms) { - this.defaultCompressionAlgorithm = defaultCompressionAlgorithm; - this.acceptableCompressionAlgorithms = Collections.unmodifiableList(acceptableCompressionAlgorithms); - } - - public CompressionAlgorithm defaultCompressionAlgorithm() { - return defaultCompressionAlgorithm; - } - - public boolean isAcceptable(int compressionAlgorithmTag) { - return isAcceptable(CompressionAlgorithm.fromId(compressionAlgorithmTag)); - } - - public boolean isAcceptable(CompressionAlgorithm compressionAlgorithm) { - return acceptableCompressionAlgorithms.contains(compressionAlgorithm); - } - - public static CompressionAlgorithmPolicy defaultCompressionAlgorithmPolicy() { - return new CompressionAlgorithmPolicy(CompressionAlgorithm.ZIP, Arrays.asList( - CompressionAlgorithm.UNCOMPRESSED, - CompressionAlgorithm.ZIP, - CompressionAlgorithm.BZIP2, - CompressionAlgorithm.ZLIB - )); - } - } - - public static final class PublicKeyAlgorithmPolicy { - - private final Map algorithmStrengths = new EnumMap<>(PublicKeyAlgorithm.class); - - public PublicKeyAlgorithmPolicy(Map minimalAlgorithmBitStrengths) { - this.algorithmStrengths.putAll(minimalAlgorithmBitStrengths); - } - - public boolean isAcceptable(int algorithmId, int bitStrength) { - return isAcceptable(PublicKeyAlgorithm.fromId(algorithmId), bitStrength); - } - - public boolean isAcceptable(PublicKeyAlgorithm algorithm, int bitStrength) { - if (!algorithmStrengths.containsKey(algorithm)) { - return false; - } - - int minStrength = algorithmStrengths.get(algorithm); - return bitStrength >= minStrength; - } - - /** - * Return PGPainless' default public key algorithm policy. - * This policy is based upon recommendations made by the German Federal Office for Information Security (BSI). - * - * Basically this policy requires keys based on elliptic curves to have a bit strength of at least 250, - * and keys based on prime number factorization / discrete logarithm problems to have a strength of at least 2000 bits. - * - * @see - * BSI - Technical Guideline - Cryptographic Mechanisms: Recommendations and Key Lengths (2021-01) - * @see BlueKrypt | Cryptographic Key Length Recommendation - * - * @return default algorithm policy - */ - public static PublicKeyAlgorithmPolicy defaultPublicKeyAlgorithmPolicy() { - Map minimalBitStrengths = new EnumMap<>(PublicKeyAlgorithm.class); - // §5.4.1 - minimalBitStrengths.put(PublicKeyAlgorithm.RSA_GENERAL, 2000); - minimalBitStrengths.put(PublicKeyAlgorithm.RSA_SIGN, 2000); - minimalBitStrengths.put(PublicKeyAlgorithm.RSA_ENCRYPT, 2000); - // Note: ElGamal is not mentioned in the BSI document. - // We assume that the requirements are similar to other DH algorithms - minimalBitStrengths.put(PublicKeyAlgorithm.ELGAMAL_ENCRYPT, 2000); - minimalBitStrengths.put(PublicKeyAlgorithm.ELGAMAL_GENERAL, 2000); - // §5.4.2 - minimalBitStrengths.put(PublicKeyAlgorithm.DSA, 2000); - // §5.4.3 - minimalBitStrengths.put(PublicKeyAlgorithm.ECDSA, 250); - // Note: EdDSA is not mentioned in the BSI document. - // We assume that the requirements are similar to other EC algorithms. - minimalBitStrengths.put(PublicKeyAlgorithm.EDDSA, 250); - // §7.2.1 - minimalBitStrengths.put(PublicKeyAlgorithm.DIFFIE_HELLMAN, 2000); - // §7.2.2 - minimalBitStrengths.put(PublicKeyAlgorithm.ECDH, 250); - minimalBitStrengths.put(PublicKeyAlgorithm.EC, 250); - - return new PublicKeyAlgorithmPolicy(minimalBitStrengths); - } - } - - /** - * Return the {@link NotationRegistry} of PGPainless. - * The notation registry is used to decide, whether a Notation is known or not. - * Background: Critical unknown notations render signatures invalid. - * - * @return Notation registry - */ - public NotationRegistry getNotationRegistry() { - return notationRegistry; - } - - /** - * Return the current {@link AlgorithmSuite} which defines preferred algorithms used during key generation. - * @return current algorithm suite - */ - public @Nonnull AlgorithmSuite getKeyGenerationAlgorithmSuite() { - return keyGenerationAlgorithmSuite; - } - - /** - * Set a custom {@link AlgorithmSuite} which defines preferred algorithms used during key generation. - * - * @param algorithmSuite custom algorithm suite - */ - public void setKeyGenerationAlgorithmSuite(@Nonnull AlgorithmSuite algorithmSuite) { - this.keyGenerationAlgorithmSuite = algorithmSuite; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/policy/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/policy/package-info.java deleted file mode 100644 index dd248151..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/policy/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Policy regarding used algorithms. - */ -package org.pgpainless.policy; diff --git a/pgpainless-core/src/main/java/org/pgpainless/provider/BouncyCastleProviderFactory.java b/pgpainless-core/src/main/java/org/pgpainless/provider/BouncyCastleProviderFactory.java deleted file mode 100644 index ace5edbe..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/provider/BouncyCastleProviderFactory.java +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.provider; - -import java.security.Provider; - -import org.bouncycastle.jce.provider.BouncyCastleProvider; - -public final class BouncyCastleProviderFactory extends ProviderFactory { - - private static final Provider provider = new BouncyCastleProvider(); - - @Override - public Provider _getProvider() { - return provider; - } - - @Override - public String _getProviderName() { - return _getProvider().getName(); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/provider/ProviderFactory.java b/pgpainless-core/src/main/java/org/pgpainless/provider/ProviderFactory.java deleted file mode 100644 index 4af67c60..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/provider/ProviderFactory.java +++ /dev/null @@ -1,35 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.provider; - -import java.security.Provider; - -public abstract class ProviderFactory { - - private static ProviderFactory FACTORY; - - protected abstract Provider _getProvider(); - protected abstract String _getProviderName(); - - public static void setFactory(ProviderFactory factory) { - ProviderFactory.FACTORY = factory; - } - - public static ProviderFactory getFactory() { - if (FACTORY == null) { - FACTORY = new BouncyCastleProviderFactory(); - } - return FACTORY; - } - - public static Provider getProvider() { - return ProviderFactory.getFactory()._getProvider(); - } - - public static String getProviderName() { - return ProviderFactory.getFactory()._getProviderName(); - } - -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/provider/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/provider/package-info.java deleted file mode 100644 index b5faa3f8..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/provider/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Classes that allow setting a custom implementation of {@link java.security.Provider}. - */ -package org.pgpainless.provider; diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/CertificateValidator.java b/pgpainless-core/src/main/java/org/pgpainless/signature/CertificateValidator.java deleted file mode 100644 index b163d61f..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/CertificateValidator.java +++ /dev/null @@ -1,272 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature; - -import static org.pgpainless.signature.SignatureVerifier.verifyOnePassSignature; - -import java.io.InputStream; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Date; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -import org.bouncycastle.bcpg.sig.KeyFlags; -import org.bouncycastle.bcpg.sig.SignerUserID; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSignature; -import org.pgpainless.algorithm.KeyFlag; -import org.pgpainless.algorithm.SignatureType; -import org.pgpainless.exception.SignatureValidationException; -import org.pgpainless.policy.Policy; -import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * A collection of static methods that validate signing certificates (public keys) and verify signature correctness. - */ -public final class CertificateValidator { - - private CertificateValidator() { - - } - - private static final Logger LOGGER = LoggerFactory.getLogger(CertificateValidator.class); - - /** - * Check if the signing key was eligible to create the provided signature. - * - * That entails: - * - Check, if the primary key is being revoked via key-revocation signatures. - * - Check, if the keys user-ids are revoked or not bound. - * - Check, if the signing subkey is revoked or expired. - * - Check, if the signing key is not capable of signing - * - * @param signature signature - * @param signingKeyRing signing key ring - * @param policy validation policy - * @return true if the signing key was eligible to create the signature - * @throws SignatureValidationException in case of a validation constraint violation - */ - public static boolean validateCertificate(PGPSignature signature, PGPPublicKeyRing signingKeyRing, Policy policy) - throws SignatureValidationException { - - Map rejections = new ConcurrentHashMap<>(); - long keyId = SignatureUtils.determineIssuerKeyId(signature); - PGPPublicKey signingSubkey = signingKeyRing.getPublicKey(keyId); - if (signingSubkey == null) { - throw new SignatureValidationException("Provided key ring does not contain a subkey with id " + Long.toHexString(keyId)); - } - - PGPPublicKey primaryKey = signingKeyRing.getPublicKey(); - - // Key-Revocation Signatures - List directKeySignatures = new ArrayList<>(); - Iterator primaryKeyRevocationIterator = primaryKey.getSignaturesOfType(SignatureType.KEY_REVOCATION.getCode()); - while (primaryKeyRevocationIterator.hasNext()) { - PGPSignature revocation = primaryKeyRevocationIterator.next(); - try { - if (SignatureVerifier.verifyKeyRevocationSignature(revocation, primaryKey, policy, signature.getCreationTime())) { - directKeySignatures.add(revocation); - } - } catch (SignatureValidationException e) { - rejections.put(revocation, e); - LOGGER.debug("Rejecting key revocation signature: {}", e.getMessage(), e); - } - } - - // Direct-Key Signatures - Iterator keySignatures = primaryKey.getSignaturesOfType(SignatureType.DIRECT_KEY.getCode()); - while (keySignatures.hasNext()) { - PGPSignature keySignature = keySignatures.next(); - try { - if (SignatureVerifier.verifyDirectKeySignature(keySignature, primaryKey, policy, signature.getCreationTime())) { - directKeySignatures.add(keySignature); - } - } catch (SignatureValidationException e) { - rejections.put(keySignature, e); - LOGGER.debug("Rejecting key signature: {}", e.getMessage(), e); - } - } - - Collections.sort(directKeySignatures, new SignatureValidityComparator(SignatureCreationDateComparator.Order.NEW_TO_OLD)); - if (!directKeySignatures.isEmpty()) { - if (directKeySignatures.get(0).getSignatureType() == SignatureType.KEY_REVOCATION.getCode()) { - throw new SignatureValidationException("Primary key has been revoked."); - } - } - - // User-ID signatures (certifications, revocations) - Iterator userIds = primaryKey.getUserIDs(); - Map> userIdSignatures = new ConcurrentHashMap<>(); - while (userIds.hasNext()) { - List signaturesOnUserId = new ArrayList<>(); - String userId = userIds.next(); - Iterator userIdSigs = primaryKey.getSignaturesForID(userId); - while (userIdSigs.hasNext()) { - PGPSignature userIdSig = userIdSigs.next(); - try { - if (SignatureVerifier.verifySignatureOverUserId(userId, userIdSig, primaryKey, policy, signature.getCreationTime())) { - signaturesOnUserId.add(userIdSig); - } - } catch (SignatureValidationException e) { - rejections.put(userIdSig, e); - LOGGER.debug("Rejecting user-id signature: {}", e.getMessage(), e); - } - } - Collections.sort(signaturesOnUserId, new SignatureValidityComparator(SignatureCreationDateComparator.Order.NEW_TO_OLD)); - userIdSignatures.put(userId, signaturesOnUserId); - } - - boolean anyUserIdValid = false; - for (String userId : userIdSignatures.keySet()) { - if (!userIdSignatures.get(userId).isEmpty()) { - PGPSignature current = userIdSignatures.get(userId).get(0); - if (current.getSignatureType() == SignatureType.CERTIFICATION_REVOCATION.getCode()) { - LOGGER.debug("User-ID '{}' is revoked.", userId); - } else { - anyUserIdValid = true; - } - } - } - - if (!anyUserIdValid) { - throw new SignatureValidationException("No valid user-id found.", rejections); - } - - // Specific signer user-id - SignerUserID signerUserID = SignatureSubpacketsUtil.getSignerUserID(signature); - if (signerUserID != null) { - PGPSignature userIdSig = userIdSignatures.get(signerUserID.getID()).get(0); - if (userIdSig.getSignatureType() == SignatureType.CERTIFICATION_REVOCATION.getCode()) { - throw new SignatureValidationException("Signature was made with user-id '" + signerUserID.getID() + "' which is revoked."); - } - } - - if (signingSubkey == primaryKey) { - if (!directKeySignatures.isEmpty()) { - if (KeyFlag.hasKeyFlag(SignatureSubpacketsUtil.getKeyFlags(directKeySignatures.get(0)).getFlags(), KeyFlag.SIGN_DATA)) { - return true; - } - } - } // Subkey Binding Signatures / Subkey Revocation Signatures - else { - List subkeySigs = new ArrayList<>(); - Iterator bindingRevocations = signingSubkey.getSignaturesOfType(SignatureType.SUBKEY_REVOCATION.getCode()); - while (bindingRevocations.hasNext()) { - PGPSignature revocation = bindingRevocations.next(); - try { - if (SignatureVerifier.verifySubkeyBindingRevocation(revocation, primaryKey, signingSubkey, policy, signature.getCreationTime())) { - subkeySigs.add(revocation); - } - } catch (SignatureValidationException e) { - rejections.put(revocation, e); - LOGGER.debug("Rejecting subkey revocation signature: {}", e.getMessage(), e); - } - } - - Iterator bindingSigs = signingSubkey.getSignaturesOfType(SignatureType.SUBKEY_BINDING.getCode()); - while (bindingSigs.hasNext()) { - PGPSignature bindingSig = bindingSigs.next(); - try { - if (SignatureVerifier.verifySubkeyBindingSignature(bindingSig, primaryKey, signingSubkey, policy, signature.getCreationTime())) { - subkeySigs.add(bindingSig); - } - } catch (SignatureValidationException e) { - rejections.put(bindingSig, e); - LOGGER.debug("Rejecting subkey binding signature: {}", e.getMessage(), e); - } - } - - Collections.sort(subkeySigs, new SignatureValidityComparator(SignatureCreationDateComparator.Order.NEW_TO_OLD)); - if (subkeySigs.isEmpty()) { - throw new SignatureValidationException("Subkey is not bound.", rejections); - } - - PGPSignature currentSig = subkeySigs.get(0); - if (currentSig.getSignatureType() == SignatureType.SUBKEY_REVOCATION.getCode()) { - throw new SignatureValidationException("Subkey is revoked."); - } - - KeyFlags keyFlags = SignatureSubpacketsUtil.getKeyFlags(currentSig); - if (keyFlags == null) { - if (directKeySignatures.isEmpty()) { - throw new SignatureValidationException("Signature was made by key which is not capable of signing (no keyflags on binding sig, no direct-key sig)."); - } - PGPSignature directKeySig = directKeySignatures.get(0); - KeyFlags directKeyFlags = SignatureSubpacketsUtil.getKeyFlags(directKeySig); - if (!KeyFlag.hasKeyFlag(directKeyFlags.getFlags(), KeyFlag.SIGN_DATA)) { - throw new SignatureValidationException("Signature was made by key which is not capable of signing (no keyflags on binding sig, no SIGN flag on direct-key sig)."); - } - } else if (!KeyFlag.hasKeyFlag(keyFlags.getFlags(), KeyFlag.SIGN_DATA)) { - throw new SignatureValidationException("Signature was made by key which is not capable of signing (no SIGN flag on binding sig)."); - } - } - return true; - } - - /** - * Validate the given signing key and then verify the given signature while parsing out the signed data. - * Uninitialized means that no signed data has been read and the hash generators state has not yet been updated. - * - * @param signature uninitialized signature - * @param signedData input stream containing signed data - * @param signingKeyRing key ring containing signing key - * @param policy validation policy - * @param validationDate date of validation - * @return true if the signature is valid, false otherwise - * @throws SignatureValidationException for validation constraint violations - */ - public static boolean validateCertificateAndVerifyUninitializedSignature(PGPSignature signature, - InputStream signedData, - PGPPublicKeyRing signingKeyRing, - Policy policy, - Date validationDate) - throws SignatureValidationException { - validateCertificate(signature, signingKeyRing, policy); - long keyId = SignatureUtils.determineIssuerKeyId(signature); - return SignatureVerifier.verifyUninitializedSignature(signature, signedData, signingKeyRing.getPublicKey(keyId), policy, validationDate); - } - - /** - * Validate the signing key and the given initialized signature. - * Initialized means that the signatures hash generator has already been updated by reading the signed data completely. - * - * @param signature initialized signature - * @param verificationKeys key ring containing the verification key - * @param policy validation policy - * @return true if the signature is valid, false otherwise - * @throws SignatureValidationException in case of a validation constraint violation - */ - public static boolean validateCertificateAndVerifyInitializedSignature(PGPSignature signature, PGPPublicKeyRing verificationKeys, Policy policy) - throws SignatureValidationException { - validateCertificate(signature, verificationKeys, policy); - long keyId = SignatureUtils.determineIssuerKeyId(signature); - PGPPublicKey signingKey = verificationKeys.getPublicKey(keyId); - SignatureVerifier.verifyInitializedSignature(signature, signingKey, policy, signature.getCreationTime()); - return true; - } - - /** - * Validate the signing key certificate and the given {@link OnePassSignatureCheck}. - * - * @param onePassSignature corresponding one-pass-signature - * @param policy policy - * @return true if the certificate is valid and the signature is correct, false otherwise. - * @throws SignatureValidationException in case of a validation error - */ - public static boolean validateCertificateAndVerifyOnePassSignature(OnePassSignatureCheck onePassSignature, Policy policy) - throws SignatureValidationException { - PGPSignature signature = onePassSignature.getSignature(); - validateCertificate(signature, onePassSignature.getVerificationKeys(), policy); - PGPPublicKey signingKey = onePassSignature.getVerificationKeys().getPublicKey(signature.getKeyID()); - verifyOnePassSignature(signature, signingKey, onePassSignature, policy); - return true; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/DetachedSignatureCheck.java b/pgpainless-core/src/main/java/org/pgpainless/signature/DetachedSignatureCheck.java deleted file mode 100644 index ab481b72..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/DetachedSignatureCheck.java +++ /dev/null @@ -1,71 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature; - -import org.bouncycastle.openpgp.PGPKeyRing; -import org.bouncycastle.openpgp.PGPSignature; -import org.pgpainless.key.OpenPgpV4Fingerprint; -import org.pgpainless.key.SubkeyIdentifier; - -/** - * Tuple-class which bundles together a signature, the signing key that created the signature, - * an identifier of the signing key and a record of whether or not the signature was verified. - */ -public class DetachedSignatureCheck { - private final PGPSignature signature; - private final PGPKeyRing signingKeyRing; - private final SubkeyIdentifier signingKeyIdentifier; - - /** - * Create a new {@link DetachedSignatureCheck} object. - * - * @param signature signature - * @param signingKeyRing signing key that created the signature - * @param signingKeyIdentifier identifier of the used signing key - */ - public DetachedSignatureCheck(PGPSignature signature, PGPKeyRing signingKeyRing, SubkeyIdentifier signingKeyIdentifier) { - this.signature = signature; - this.signingKeyRing = signingKeyRing; - this.signingKeyIdentifier = signingKeyIdentifier; - } - - /** - * Return the OpenPGP signature. - * - * @return signature - */ - public PGPSignature getSignature() { - return signature; - } - - /** - * Return an identifier pointing to the exact signing key which was used to create this signature. - * - * @return signing key identifier - */ - public SubkeyIdentifier getSigningKeyIdentifier() { - return signingKeyIdentifier; - } - - /** - * Return the key ring that contains the signing key that created this signature. - * - * @return key ring - */ - public PGPKeyRing getSigningKeyRing() { - return signingKeyRing; - } - - /** - * Return the {@link OpenPgpV4Fingerprint} of the key that created the signature. - * - * @return fingerprint of the signing key - * @deprecated use {@link #getSigningKeyIdentifier()} instead. - */ - @Deprecated - public OpenPgpV4Fingerprint getFingerprint() { - return signingKeyIdentifier.getSubkeyFingerprint(); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/OnePassSignatureCheck.java b/pgpainless-core/src/main/java/org/pgpainless/signature/OnePassSignatureCheck.java deleted file mode 100644 index 4fcfe239..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/OnePassSignatureCheck.java +++ /dev/null @@ -1,73 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature; - -import org.bouncycastle.openpgp.PGPOnePassSignature; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSignature; -import org.pgpainless.key.OpenPgpV4Fingerprint; -import org.pgpainless.key.SubkeyIdentifier; - -/** - * Tuple-class that bundles together a {@link PGPOnePassSignature} object, a {@link PGPPublicKeyRing} - * destined to verify the signature, the {@link PGPSignature} itself and a record of whether the signature - * was verified. - */ -public class OnePassSignatureCheck { - private final PGPOnePassSignature onePassSignature; - private final PGPPublicKeyRing verificationKeys; - private PGPSignature signature; - - /** - * Create a new {@link OnePassSignatureCheck}. - * - * @param onePassSignature one-pass signature packet used to initialize the signature verifier. - * @param verificationKeys verification keys - */ - public OnePassSignatureCheck(PGPOnePassSignature onePassSignature, PGPPublicKeyRing verificationKeys) { - this.onePassSignature = onePassSignature; - this.verificationKeys = verificationKeys; - } - - public void setSignature(PGPSignature signature) { - this.signature = signature; - } - - /** - * Return the {@link PGPOnePassSignature} object. - * - * @return onePassSignature - */ - public PGPOnePassSignature getOnePassSignature() { - return onePassSignature; - } - - /** - * Return the {@link OpenPgpV4Fingerprint} of the signing key. - * - * @return signing key fingerprint - */ - public SubkeyIdentifier getSigningKey() { - return new SubkeyIdentifier(verificationKeys, onePassSignature.getKeyID()); - } - - /** - * Return the signature. - * - * @return signature - */ - public PGPSignature getSignature() { - return signature; - } - - /** - * Return the key ring used to verify the signature. - * - * @return verification keys - */ - public PGPPublicKeyRing getVerificationKeys() { - return verificationKeys; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureCreationDateComparator.java b/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureCreationDateComparator.java deleted file mode 100644 index 8900be40..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureCreationDateComparator.java +++ /dev/null @@ -1,53 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature; - -import java.util.Comparator; - -import org.bouncycastle.openpgp.PGPSignature; - -/** - * Comparator which can be used to sort signatures with regard to their creation time. - */ -public class SignatureCreationDateComparator implements Comparator { - - public static final Order DEFAULT_ORDER = Order.OLD_TO_NEW; - - public enum Order { - /** - * Oldest signatures first. - */ - OLD_TO_NEW, - - /** - * Newest signatures first. - */ - NEW_TO_OLD - } - - private final Order order; - - /** - * Create a new comparator which sorts signatures old to new. - */ - public SignatureCreationDateComparator() { - this(DEFAULT_ORDER); - } - - /** - * Create a new comparator which sorts signatures according to the passed ordering. - * @param order ordering - */ - public SignatureCreationDateComparator(Order order) { - this.order = order; - } - - @Override - public int compare(PGPSignature one, PGPSignature two) { - return order == Order.OLD_TO_NEW - ? one.getCreationTime().compareTo(two.getCreationTime()) - : two.getCreationTime().compareTo(one.getCreationTime()); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/SignaturePicker.java b/pgpainless-core/src/main/java/org/pgpainless/signature/SignaturePicker.java deleted file mode 100644 index b5bb9706..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/SignaturePicker.java +++ /dev/null @@ -1,375 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature; - -import java.util.Collections; -import java.util.Date; -import java.util.Iterator; -import java.util.List; - -import org.bouncycastle.openpgp.PGPKeyRing; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPSignature; -import org.pgpainless.algorithm.SignatureType; -import org.pgpainless.exception.SignatureValidationException; -import org.pgpainless.policy.Policy; -import org.pgpainless.util.CollectionUtils; - -/** - * Pick signatures from keys. - * - * The format of a V4 OpenPGP key is: - * - * Primary-Key - * [Revocation Self Signature] - * [Direct Key Signature...] - * User ID [Signature ...] - * [User ID [Signature ...] ...] - * [User Attribute [Signature ...] ...] - * [[Subkey [Binding-Signature-Revocation] Primary-Key-Binding-Signature] ...] - */ -public final class SignaturePicker { - - private SignaturePicker() { - - } - - /** - * Pick the, at validation date most recent valid key revocation signature. - * If there are hard revocation signatures, the latest hard revocation sig is picked, even if it was created after - * validationDate or if it is already expired. - * - * @param keyRing key ring - * @return most recent, valid key revocation signature - */ - public static PGPSignature pickCurrentRevocationSelfSignature(PGPKeyRing keyRing, Policy policy, Date validationDate) { - PGPPublicKey primaryKey = keyRing.getPublicKey(); - - List signatures = getSortedSignaturesOfType(primaryKey, SignatureType.KEY_REVOCATION); - PGPSignature mostCurrentValidSig = null; - - for (PGPSignature signature : signatures) { - try { - SignatureVerifier.verifyKeyRevocationSignature(signature, primaryKey, policy, validationDate); - } catch (SignatureValidationException e) { - // Signature is not valid - continue; - } - mostCurrentValidSig = signature; - } - - return mostCurrentValidSig; - } - - /** - * Pick the, at validationDate most recent, valid direct key signature. - * This method might return null, if there is no direct key self-signature which is valid at validationDate. - * - * @param keyRing key ring - * @param validationDate validation date - * @return direct-key self-signature - */ - public static PGPSignature pickCurrentDirectKeySelfSignature(PGPKeyRing keyRing, Policy policy, Date validationDate) { - PGPPublicKey primaryKey = keyRing.getPublicKey(); - return pickCurrentDirectKeySignature(primaryKey, primaryKey, policy, validationDate); - } - - /** - * Pick the, at validationDate, latest, valid direct key signature made by signingKey on signedKey. - * This method might return null, if there is no direct key self signature which is valid at validationDate. - * - * @param signingKey key that created the signature - * @param signedKey key that carries the signature - * @param validationDate validation date - * @return direct key sig - */ - public static PGPSignature pickCurrentDirectKeySignature(PGPPublicKey signingKey, PGPPublicKey signedKey, Policy policy, Date validationDate) { - List directKeySignatures = getSortedSignaturesOfType(signedKey, SignatureType.DIRECT_KEY); - - PGPSignature mostRecentDirectKeySigBySigningKey = null; - for (PGPSignature signature : directKeySignatures) { - try { - SignatureVerifier.verifyDirectKeySignature(signature, signingKey, signedKey, policy, validationDate); - } catch (SignatureValidationException e) { - // Direct key sig is not valid - continue; - } - mostRecentDirectKeySigBySigningKey = signature; - } - - return mostRecentDirectKeySigBySigningKey; - } - - /** - * Pick the, at validationDate, latest direct key signature. - * This method might return an expired signature. - * If there are more than one direct-key signature, and some of those are not expired, the latest non-expired - * yet already effective direct-key signature will be returned. - * - * @param keyRing key ring - * @param validationDate validation date - * @return latest direct key signature - */ - public static PGPSignature pickLatestDirectKeySignature(PGPKeyRing keyRing, Policy policy, Date validationDate) { - PGPPublicKey primaryKey = keyRing.getPublicKey(); - return pickLatestDirectKeySignature(primaryKey, primaryKey, policy, validationDate); - } - - /** - * Pick the, at validationDate, latest direct key signature made by signingKey on signedKey. - * This method might return an expired signature. - * If a non-expired direct-key signature exists, the latest non-expired yet already effective direct-key - * signature will be returned. - * - * @param signingKey signing key (key that made the sig) - * @param signedKey signed key (key that carries the sig) - * @param validationDate date of validation - * @return latest direct key sig - */ - public static PGPSignature pickLatestDirectKeySignature(PGPPublicKey signingKey, PGPPublicKey signedKey, Policy policy, Date validationDate) { - List signatures = getSortedSignaturesOfType(signedKey, SignatureType.DIRECT_KEY); - - PGPSignature latestDirectKeySignature = null; - for (PGPSignature signature : signatures) { - try { - SignatureValidator.signatureIsOfType(SignatureType.DIRECT_KEY).verify(signature); - SignatureValidator.signatureStructureIsAcceptable(signingKey, policy).verify(signature); - SignatureValidator.signatureIsAlreadyEffective(validationDate).verify(signature); - // if the currently latest signature is not yet expired, check if the next candidate is not yet expired - if (latestDirectKeySignature != null && !SignatureUtils.isSignatureExpired(latestDirectKeySignature, validationDate)) { - SignatureValidator.signatureIsNotYetExpired(validationDate).verify(signature); - } - SignatureValidator.correctSignatureOverKey(signingKey, signedKey).verify(signature); - } catch (SignatureValidationException e) { - // Direct key signature is not valid - continue; - } - latestDirectKeySignature = signature; - } - - return latestDirectKeySignature; - } - - /** - * Pick the, at validationDate most recent, valid user-id revocation signature. - * If there are hard revocation signatures, the latest hard revocation sig is picked, even if it was created after - * validationDate or if it is already expired. - * - * @param keyRing key ring - * @param userId user-Id that gets revoked - * @param validationDate validation date - * @return revocation signature - */ - public static PGPSignature pickCurrentUserIdRevocationSignature(PGPKeyRing keyRing, String userId, Policy policy, Date validationDate) { - PGPPublicKey primaryKey = keyRing.getPublicKey(); - List signatures = getSortedSignaturesOfType(primaryKey, SignatureType.CERTIFICATION_REVOCATION); - - PGPSignature latestUserIdRevocation = null; - for (PGPSignature signature : signatures) { - try { - SignatureVerifier.verifyUserIdRevocation(userId, signature, primaryKey, policy, validationDate); - } catch (SignatureValidationException e) { - // User-id revocation is not valid - continue; - } - latestUserIdRevocation = signature; - } - - return latestUserIdRevocation; - } - - /** - * Pick the, at validationDate latest, valid certification self-signature for the given user-id. - * This method might return null, if there is no certification self signature for that user-id which is valid - * at validationDate. - * - * @param keyRing keyring - * @param userId userid - * @param validationDate validation date - * @return user-id certification - */ - public static PGPSignature pickCurrentUserIdCertificationSignature(PGPKeyRing keyRing, String userId, Policy policy, Date validationDate) { - PGPPublicKey primaryKey = keyRing.getPublicKey(); - - Iterator userIdSigIterator = primaryKey.getSignaturesForID(userId); - List signatures = CollectionUtils.iteratorToList(userIdSigIterator); - Collections.sort(signatures, new SignatureCreationDateComparator()); - - PGPSignature mostRecentUserIdCertification = null; - for (PGPSignature signature : signatures) { - try { - SignatureVerifier.verifyUserIdCertification(userId, signature, primaryKey, policy, validationDate); - } catch (SignatureValidationException e) { - // User-id certification is not valid - continue; - } - mostRecentUserIdCertification = signature; - } - - return mostRecentUserIdCertification; - } - - /** - * Pick the, at validationDate latest certification self-signature for the given user-id. - * This method might return an expired signature. - * If a non-expired user-id certification signature exists, the latest non-expired yet already effective - * user-id certification signature for the given user-id will be returned. - * - * @param keyRing keyring - * @param userId userid - * @param validationDate validation date - * @return user-id certification - */ - public static PGPSignature pickLatestUserIdCertificationSignature(PGPKeyRing keyRing, String userId, Policy policy, Date validationDate) { - PGPPublicKey primaryKey = keyRing.getPublicKey(); - - Iterator userIdSigIterator = primaryKey.getSignaturesForID(userId); - List signatures = CollectionUtils.iteratorToList(userIdSigIterator); - Collections.sort(signatures, new SignatureCreationDateComparator()); - - PGPSignature latestUserIdCert = null; - for (PGPSignature signature : signatures) { - try { - SignatureValidator.wasPossiblyMadeByKey(primaryKey).verify(signature); - SignatureValidator.signatureIsCertification().verify(signature); - SignatureValidator.signatureStructureIsAcceptable(primaryKey, policy).verify(signature); - SignatureValidator.signatureIsAlreadyEffective(validationDate).verify(signature); - // if the currently latest signature is not yet expired, check if the next candidate is not yet expired - if (latestUserIdCert != null && !SignatureUtils.isSignatureExpired(latestUserIdCert, validationDate)) { - SignatureValidator.signatureIsNotYetExpired(validationDate).verify(signature); - } - SignatureValidator.correctSignatureOverUserId(userId, primaryKey, primaryKey).verify(signature); - } catch (SignatureValidationException e) { - // User-id certification is not valid - continue; - } - - latestUserIdCert = signature; - } - - return latestUserIdCert; - } - - /** - * Pick the, at validationDate most recent, valid subkey revocation signature. - * If there are hard revocation signatures, the latest hard revocation sig is picked, even if it was created after - * validationDate or if it is already expired. - * - * @param keyRing keyring - * @param subkey subkey - * @param validationDate validation date - * @return subkey revocation signature - */ - public static PGPSignature pickCurrentSubkeyBindingRevocationSignature(PGPKeyRing keyRing, PGPPublicKey subkey, Policy policy, Date validationDate) { - PGPPublicKey primaryKey = keyRing.getPublicKey(); - if (primaryKey.getKeyID() == subkey.getKeyID()) { - throw new IllegalArgumentException("Primary key cannot have subkey binding revocations."); - } - - List signatures = getSortedSignaturesOfType(subkey, SignatureType.SUBKEY_REVOCATION); - PGPSignature latestSubkeyRevocation = null; - - for (PGPSignature signature : signatures) { - try { - SignatureVerifier.verifySubkeyBindingRevocation(signature, primaryKey, subkey, policy, validationDate); - } catch (SignatureValidationException e) { - // subkey binding revocation is not valid - continue; - } - latestSubkeyRevocation = signature; - } - - return latestSubkeyRevocation; - } - - /** - * Pick the, at validationDate latest, valid subkey binding signature for the given subkey. - * This method might return null, if there is no subkey binding signature which is valid - * at validationDate. - * - * @param keyRing key ring - * @param subkey subkey - * @param validationDate date of validation - * @return most recent valid subkey binding signature - */ - public static PGPSignature pickCurrentSubkeyBindingSignature(PGPKeyRing keyRing, PGPPublicKey subkey, Policy policy, Date validationDate) { - PGPPublicKey primaryKey = keyRing.getPublicKey(); - if (primaryKey.getKeyID() == subkey.getKeyID()) { - throw new IllegalArgumentException("Primary key cannot have subkey binding signature."); - } - - List subkeyBindingSigs = getSortedSignaturesOfType(subkey, SignatureType.SUBKEY_BINDING); - PGPSignature mostCurrentValidSig = null; - - for (PGPSignature signature : subkeyBindingSigs) { - try { - SignatureVerifier.verifySubkeyBindingSignature(signature, primaryKey, subkey, policy, validationDate); - } catch (SignatureValidationException validationException) { - // Subkey binding sig is not valid - continue; - } - mostCurrentValidSig = signature; - } - - return mostCurrentValidSig; - } - - /** - * Pick the, at validationDate latest subkey binding signature for the given subkey. - * This method might return an expired signature. - * If a non-expired subkey binding signature exists, the latest non-expired yet already effective - * subkey binding signature for the given subkey will be returned. - * - * @param keyRing key ring - * @param subkey subkey - * @param validationDate validationDate - * @return subkey binding signature - */ - public static PGPSignature pickLatestSubkeyBindingSignature(PGPKeyRing keyRing, PGPPublicKey subkey, Policy policy, Date validationDate) { - PGPPublicKey primaryKey = keyRing.getPublicKey(); - if (primaryKey.getKeyID() == subkey.getKeyID()) { - throw new IllegalArgumentException("Primary key cannot have subkey binding signature."); - } - - List signatures = getSortedSignaturesOfType(subkey, SignatureType.SUBKEY_BINDING); - PGPSignature latestSubkeyBinding = null; - - for (PGPSignature signature : signatures) { - try { - SignatureValidator.signatureIsOfType(SignatureType.SUBKEY_BINDING).verify(signature); - SignatureValidator.signatureStructureIsAcceptable(primaryKey, policy).verify(signature); - SignatureValidator.signatureIsAlreadyEffective(validationDate).verify(signature); - // if the currently latest signature is not yet expired, check if the next candidate is not yet expired - if (latestSubkeyBinding != null && !SignatureUtils.isSignatureExpired(latestSubkeyBinding, validationDate)) { - SignatureValidator.signatureIsNotYetExpired(validationDate).verify(signature); - } - SignatureValidator.correctSubkeyBindingSignature(primaryKey, subkey).verify(signature); - } catch (SignatureValidationException e) { - // Subkey binding sig is not valid - continue; - } - latestSubkeyBinding = signature; - } - - return latestSubkeyBinding; - } - - /** - * Return a list of all signatures of the given {@link SignatureType} on the given key, sorted using a - * {@link SignatureCreationDateComparator}. - * - * The returned list will be sorted first by ascending signature creation time. - * - * @param key key - * @param type type of signatures which shall be collected and sorted - * @return sorted list of signatures - */ - private static List getSortedSignaturesOfType(PGPPublicKey key, SignatureType type) { - Iterator signaturesOfType = key.getSignaturesOfType(type.getCode()); - List signatureList = CollectionUtils.iteratorToList(signaturesOfType); - Collections.sort(signatureList, new SignatureCreationDateComparator()); - return signatureList; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureUtils.java b/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureUtils.java deleted file mode 100644 index ff05ca9f..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureUtils.java +++ /dev/null @@ -1,337 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.Charset; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; - -import org.bouncycastle.bcpg.sig.IssuerKeyID; -import org.bouncycastle.bcpg.sig.KeyExpirationTime; -import org.bouncycastle.bcpg.sig.RevocationReason; -import org.bouncycastle.bcpg.sig.SignatureExpirationTime; -import org.bouncycastle.openpgp.PGPCompressedData; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPMarker; -import org.bouncycastle.openpgp.PGPObjectFactory; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.PGPSignatureGenerator; -import org.bouncycastle.openpgp.PGPSignatureList; -import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder; -import org.bouncycastle.util.encoders.Hex; -import org.pgpainless.PGPainless; -import org.pgpainless.algorithm.HashAlgorithm; -import org.pgpainless.algorithm.SignatureType; -import org.pgpainless.implementation.ImplementationFactory; -import org.pgpainless.key.OpenPgpV4Fingerprint; -import org.pgpainless.key.util.OpenPgpKeyAttributeUtil; -import org.pgpainless.key.util.RevocationAttributes; -import org.pgpainless.policy.Policy; -import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil; -import org.pgpainless.util.ArmorUtils; - -/** - * Utility methods related to signatures. - */ -public final class SignatureUtils { - - private SignatureUtils() { - - } - - /** - * Return a signature generator for the provided signing key. - * The signature generator will follow the hash algorithm preferences of the signing key and pick the best algorithm. - * - * @param singingKey signing key - * @return signature generator - */ - public static PGPSignatureGenerator getSignatureGeneratorFor(PGPSecretKey singingKey) { - return getSignatureGeneratorFor(singingKey.getPublicKey()); - } - - /** - * Return a signature generator for the provided signing key. - * The signature generator will follow the hash algorithm preferences of the signing key and pick the best algorithm. - * - * @param signingPubKey signing key - * @return signature generator - */ - public static PGPSignatureGenerator getSignatureGeneratorFor(PGPPublicKey signingPubKey) { - PGPSignatureGenerator signatureGenerator = new PGPSignatureGenerator( - getPgpContentSignerBuilderForKey(signingPubKey)); - return signatureGenerator; - } - - /** - * Return a content signer builder for the passed public key. - * - * The content signer will use a hash algorithm derived from the keys algorithm preferences. - * If no preferences can be derived, the key will fall back to the default hash algorithm as set in - * the {@link org.pgpainless.policy.Policy}. - * - * TODO: Move negotiation to negotiator class - * - * @param publicKey public key - * @return content signer builder - */ - private static PGPContentSignerBuilder getPgpContentSignerBuilderForKey(PGPPublicKey publicKey) { - List preferredHashAlgorithms = OpenPgpKeyAttributeUtil.getPreferredHashAlgorithms(publicKey); - if (preferredHashAlgorithms.isEmpty()) { - preferredHashAlgorithms = OpenPgpKeyAttributeUtil.guessPreferredHashAlgorithms(publicKey); - } - HashAlgorithm hashAlgorithm = negotiateHashAlgorithm(preferredHashAlgorithms); - - return ImplementationFactory.getInstance().getPGPContentSignerBuilder(publicKey.getAlgorithm(), hashAlgorithm.getAlgorithmId()); - } - - /** - * Negotiate an acceptable hash algorithm from the provided list of options. - * Acceptance of hash algorithms can be changed by setting a custom {@link Policy}. - * - * @param preferredHashAlgorithms list of preferred hash algorithms of a key - * @return first acceptable algorithm, or policies default hash algorithm - */ - private static HashAlgorithm negotiateHashAlgorithm(List preferredHashAlgorithms) { - Policy policy = PGPainless.getPolicy(); - for (HashAlgorithm option : preferredHashAlgorithms) { - if (policy.getSignatureHashAlgorithmPolicy().isAcceptable(option)) { - return option; - } - } - - return PGPainless.getPolicy().getSignatureHashAlgorithmPolicy().defaultHashAlgorithm(); - } - - /** - * Extract and return the key expiration date value from the given signature. - * If the signature does not carry a {@link KeyExpirationTime} subpacket, return null. - * - * @param keyCreationDate creation date of the key - * @param signature signature - * @return key expiration date as given by the signature - */ - public static Date getKeyExpirationDate(Date keyCreationDate, PGPSignature signature) { - KeyExpirationTime keyExpirationTime = SignatureSubpacketsUtil.getKeyExpirationTime(signature); - long expiresInSecs = keyExpirationTime == null ? 0 : keyExpirationTime.getTime(); - return datePlusSeconds(keyCreationDate, expiresInSecs); - } - - /** - * Return the expiration date of the signature. - * If the signature has no expiration date, {@link #datePlusSeconds(Date, long)} will return null. - * - * @param signature signature - * @return expiration date of the signature, or null if it does not expire. - */ - public static Date getSignatureExpirationDate(PGPSignature signature) { - Date creationDate = signature.getCreationTime(); - SignatureExpirationTime signatureExpirationTime = SignatureSubpacketsUtil.getSignatureExpirationTime(signature); - long expiresInSecs = signatureExpirationTime == null ? 0 : signatureExpirationTime.getTime(); - return datePlusSeconds(creationDate, expiresInSecs); - } - - /** - * Return a new date which represents the given date plus the given amount of seconds added. - * - * Since '0' is a special value in the OpenPGP specification when it comes to dates - * (e.g. '0' means no expiration for expiration dates), this method will return 'null' if seconds is 0. - * - * @param date date - * @param seconds number of seconds to be added - * @return date plus seconds or null if seconds is '0' - */ - public static Date datePlusSeconds(Date date, long seconds) { - if (seconds == 0) { - return null; - } - return new Date(date.getTime() + 1000 * seconds); - } - - /** - * Return true, if the expiration date of the {@link PGPSignature} lays in the past. - * If no expiration date is present in the signature, it is considered non-expired. - * - * @param signature signature - * @return true if expired, false otherwise - */ - public static boolean isSignatureExpired(PGPSignature signature) { - return isSignatureExpired(signature, new Date()); - } - - /** - * Return true, if the expiration date of the given {@link PGPSignature} is past the given comparison {@link Date}. - * If no expiration date is present in the signature, it is considered non-expiring. - * - * @param signature signature - * @param comparisonDate reference date - * @return true if sig is expired at reference date, false otherwise - */ - public static boolean isSignatureExpired(PGPSignature signature, Date comparisonDate) { - Date expirationDate = getSignatureExpirationDate(signature); - return expirationDate != null && comparisonDate.after(expirationDate); - } - - /** - * Return true if the provided signature is a hard revocation. - * Hard revocations are revocation signatures which either carry a revocation reason of - * {@link RevocationAttributes.Reason#KEY_COMPROMISED} or {@link RevocationAttributes.Reason#NO_REASON}, - * or no reason at all. - * - * @param signature signature - * @return true if signature is a hard revocation - */ - public static boolean isHardRevocation(PGPSignature signature) { - - SignatureType type = SignatureType.valueOf(signature.getSignatureType()); - if (type != SignatureType.KEY_REVOCATION && type != SignatureType.SUBKEY_REVOCATION && type != SignatureType.CERTIFICATION_REVOCATION) { - // Not a revocation - return false; - } - - RevocationReason reasonSubpacket = SignatureSubpacketsUtil.getRevocationReason(signature); - if (reasonSubpacket == null) { - // no reason -> hard revocation - return true; - } - return RevocationAttributes.Reason.isHardRevocation(reasonSubpacket.getRevocationReason()); - } - - /** - * Parse an ASCII encoded list of OpenPGP signatures into a {@link PGPSignatureList} - * and return it as a {@link List}. - * - * @param encodedSignatures ASCII armored signature list - * @return signature list - * @throws IOException if the signatures cannot be read - */ - public static List readSignatures(String encodedSignatures) throws IOException, PGPException { - byte[] bytes = encodedSignatures.getBytes(Charset.forName("UTF8")); - return readSignatures(bytes); - } - - /** - * Read a single, or a list of {@link PGPSignature PGPSignatures} and return them as a {@link List}. - * - * @param encodedSignatures ASCII armored or binary signatures - * @return signatures - * @throws IOException if the signatures cannot be read - * @throws PGPException in case of an OpenPGP error - */ - public static List readSignatures(byte[] encodedSignatures) throws IOException, PGPException { - InputStream inputStream = new ByteArrayInputStream(encodedSignatures); - return readSignatures(inputStream); - } - - /** - * Read and return {@link PGPSignature PGPSignatures}. - * This method can deal with signatures that may be armored, compressed and may contain marker packets. - * - * @param inputStream input stream - * @return list of encountered signatures - * @throws IOException in case of a stream error - * @throws PGPException in case of an OpenPGP error - */ - public static List readSignatures(InputStream inputStream) throws IOException, PGPException { - List signatures = new ArrayList<>(); - InputStream pgpIn = ArmorUtils.getDecoderStream(inputStream); - PGPObjectFactory objectFactory = new PGPObjectFactory( - pgpIn, ImplementationFactory.getInstance().getKeyFingerprintCalculator()); - - Object nextObject = tryNext(objectFactory); - while (nextObject != null) { - if (nextObject instanceof PGPMarker) { - nextObject = tryNext(objectFactory); - continue; - } - if (nextObject instanceof PGPCompressedData) { - PGPCompressedData compressedData = (PGPCompressedData) nextObject; - objectFactory = new PGPObjectFactory(compressedData.getDataStream(), - ImplementationFactory.getInstance().getKeyFingerprintCalculator()); - nextObject = tryNext(objectFactory); - continue; - } - if (nextObject instanceof PGPSignatureList) { - PGPSignatureList signatureList = (PGPSignatureList) nextObject; - for (PGPSignature s : signatureList) { - signatures.add(s); - } - } - if (nextObject instanceof PGPSignature) { - signatures.add((PGPSignature) nextObject); - } - nextObject = tryNext(objectFactory); - } - pgpIn.close(); - - return signatures; - } - - /** - * Try reading the next signature from the factory. - * - * This is a helper method for BC choking on unexpected data like invalid signature versions. - * Unfortunately, this solves only half the issue, see bcgit/bc-java#1006 for a proper fix. - * - * @see BC-Java: Ignore PGPSignature with invalid version - * - * @param factory pgp object factory - * @return next non-throwing object or null - * @throws IOException in case of a stream error - */ - private static Object tryNext(PGPObjectFactory factory) throws IOException { - try { - Object o = factory.nextObject(); - return o; - } catch (RuntimeException e) { - return tryNext(factory); - } - } - - /** - * Determine the issuer key-id of a {@link PGPSignature}. - * This method first inspects the {@link IssuerKeyID} subpacket of the signature and returns the key-id if present. - * If not, it inspects the {@link org.bouncycastle.bcpg.sig.IssuerFingerprint} packet and retrieves the key-id from the fingerprint. - * - * Otherwise it returns 0. - * @param signature signature - * @return signatures issuing key id - */ - public static long determineIssuerKeyId(PGPSignature signature) { - IssuerKeyID issuerKeyId = SignatureSubpacketsUtil.getIssuerKeyId(signature); - OpenPgpV4Fingerprint fingerprint = SignatureSubpacketsUtil.getIssuerFingerprintAsOpenPgpV4Fingerprint(signature); - if (issuerKeyId != null && issuerKeyId.getKeyID() != 0) { - return issuerKeyId.getKeyID(); - } - if (issuerKeyId == null && fingerprint != null) { - return fingerprint.getKeyId(); - } - return 0; - } - - /** - * Return the digest prefix of the signature as hex-encoded String. - * - * @param signature signature - * @return digest prefix - */ - public static String getSignatureDigestPrefix(PGPSignature signature) { - return Hex.toHexString(signature.getDigestPrefix()); - } - - public static List toList(PGPSignatureList signatures) { - List list = new ArrayList<>(); - for (PGPSignature signature : signatures) { - list.add(signature); - } - return list; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureValidator.java b/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureValidator.java deleted file mode 100644 index 69a7a80a..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureValidator.java +++ /dev/null @@ -1,628 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature; - -import java.security.NoSuchAlgorithmException; -import java.util.Arrays; -import java.util.Date; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -import org.bouncycastle.bcpg.sig.KeyFlags; -import org.bouncycastle.bcpg.sig.NotationData; -import org.bouncycastle.bcpg.sig.SignatureCreationTime; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.PGPSignatureList; -import org.bouncycastle.openpgp.PGPSignatureSubpacketVector; -import org.bouncycastle.openpgp.PGPUserAttributeSubpacketVector; -import org.pgpainless.algorithm.HashAlgorithm; -import org.pgpainless.algorithm.KeyFlag; -import org.pgpainless.algorithm.PublicKeyAlgorithm; -import org.pgpainless.algorithm.SignatureSubpacket; -import org.pgpainless.algorithm.SignatureType; -import org.pgpainless.exception.SignatureValidationException; -import org.pgpainless.implementation.ImplementationFactory; -import org.pgpainless.key.OpenPgpV4Fingerprint; -import org.pgpainless.policy.Policy; -import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil; -import org.pgpainless.util.BCUtil; -import org.pgpainless.util.DateUtil; -import org.pgpainless.util.NotationRegistry; - -/** - * A collection of validators that perform validation steps over signatures. - */ -public abstract class SignatureValidator { - - public abstract void verify(PGPSignature signature) throws SignatureValidationException; - - /** - * Check, whether there is the possibility that the given signature was created by the given key. - * {@link #verify(PGPSignature)} throws a {@link SignatureValidationException} if we can say with certainty that the signature - * was not created by the given key (e.g. if the sig carries another issuer, issuer fingerprint packet). - * - * If there is no information found in the signature about who created it (no issuer, no fingerprint), - * {@link #verify(PGPSignature)} will simply return since it is plausible that the given key created the sig. - * - * @param signingKey signing key - * @return validator that throws a {@link SignatureValidationException} if the signature was not possibly made by the given key. - */ - public static SignatureValidator wasPossiblyMadeByKey(PGPPublicKey signingKey) { - return new SignatureValidator() { - @Override - public void verify(PGPSignature signature) throws SignatureValidationException { - OpenPgpV4Fingerprint signingKeyFingerprint = new OpenPgpV4Fingerprint(signingKey); - - Long issuer = SignatureSubpacketsUtil.getIssuerKeyIdAsLong(signature); - if (issuer != null) { - if (issuer != signingKey.getKeyID()) { - throw new SignatureValidationException("Signature was not created by " + signingKeyFingerprint + " (signature issuer: " + Long.toHexString(issuer) + ")"); - } - } - - OpenPgpV4Fingerprint fingerprint = SignatureSubpacketsUtil.getIssuerFingerprintAsOpenPgpV4Fingerprint(signature); - if (fingerprint != null) { - if (!fingerprint.equals(signingKeyFingerprint)) { - throw new SignatureValidationException("Signature was not created by " + signingKeyFingerprint + " (signature fingerprint: " + fingerprint + ")"); - } - } - - // No issuer information found, so we cannot rule out that we did not create the sig - } - }; - - } - - /** - * Verify that a subkey binding signature - if the subkey is signing-capable - contains a valid primary key - * binding signature. - * - * @param primaryKey primary key - * @param subkey subkey - * @param policy policy - * @param validationDate reference date for signature verification - * @return validator - */ - public static SignatureValidator hasValidPrimaryKeyBindingSignatureIfRequired(PGPPublicKey primaryKey, PGPPublicKey subkey, Policy policy, Date validationDate) { - return new SignatureValidator() { - @Override - public void verify(PGPSignature signature) throws SignatureValidationException { - if (!PublicKeyAlgorithm.fromId(signature.getKeyAlgorithm()).isSigningCapable()) { - // subkey is not signing capable -> No need to process embedded sigs - return; - } - - KeyFlags keyFlags = SignatureSubpacketsUtil.getKeyFlags(signature); - if (keyFlags == null) { - return; - } - if (!KeyFlag.hasKeyFlag(keyFlags.getFlags(), KeyFlag.SIGN_DATA) - && !KeyFlag.hasKeyFlag(keyFlags.getFlags(), KeyFlag.CERTIFY_OTHER)) { - return; - } - - try { - PGPSignatureList embeddedSignatures = SignatureSubpacketsUtil.getEmbeddedSignature(signature); - boolean hasValidPrimaryKeyBinding = false; - Map rejectedEmbeddedSigs = new ConcurrentHashMap<>(); - for (PGPSignature embedded : embeddedSignatures) { - - if (SignatureType.valueOf(embedded.getSignatureType()) == SignatureType.PRIMARYKEY_BINDING) { - - try { - signatureStructureIsAcceptable(subkey, policy).verify(embedded); - signatureIsEffective(validationDate).verify(embedded); - correctPrimaryKeyBindingSignature(primaryKey, subkey).verify(embedded); - - hasValidPrimaryKeyBinding = true; - break; - } catch (SignatureValidationException e) { - rejectedEmbeddedSigs.put(embedded, e); - } - } - } - - if (!hasValidPrimaryKeyBinding) { - throw new SignatureValidationException("Missing primary key binding signature on signing capable subkey " + Long.toHexString(subkey.getKeyID()), rejectedEmbeddedSigs); - } - } catch (PGPException e) { - throw new SignatureValidationException("Cannot process list of embedded signatures.", e); - } - } - }; - } - - /** - * Verify that a signature has an acceptable structure. - * - * @param signingKey signing key - * @param policy policy - * @return validator - */ - public static SignatureValidator signatureStructureIsAcceptable(PGPPublicKey signingKey, Policy policy) { - return new SignatureValidator() { - @Override - public void verify(PGPSignature signature) throws SignatureValidationException { - signatureIsNotMalformed(signingKey).verify(signature); - signatureDoesNotHaveCriticalUnknownNotations(policy.getNotationRegistry()).verify(signature); - signatureDoesNotHaveCriticalUnknownSubpackets().verify(signature); - signatureUsesAcceptableHashAlgorithm(policy).verify(signature); - signatureUsesAcceptablePublicKeyAlgorithm(policy, signingKey).verify(signature); - } - }; - } - - /** - * Verify that a signature was made using an acceptable {@link PublicKeyAlgorithm}. - * - * @param policy policy - * @param signingKey signing key - * @return validator - */ - private static SignatureValidator signatureUsesAcceptablePublicKeyAlgorithm(Policy policy, PGPPublicKey signingKey) { - return new SignatureValidator() { - @Override - public void verify(PGPSignature signature) throws SignatureValidationException { - PublicKeyAlgorithm algorithm = PublicKeyAlgorithm.fromId(signingKey.getAlgorithm()); - try { - int bitStrength = BCUtil.getBitStrength(signingKey); - if (!policy.getPublicKeyAlgorithmPolicy().isAcceptable(algorithm, bitStrength)) { - throw new SignatureValidationException("Signature was made using unacceptable key. " + - algorithm + " (" + bitStrength + " bits) is not acceptable according to the public key algorithm policy."); - } - } catch (NoSuchAlgorithmException e) { - throw new SignatureValidationException("Cannot determine bit strength of signing key.", e); - } - } - }; - } - - /** - * Verify that a signature uses an acceptable {@link HashAlgorithm}. - * - * @param policy policy - * @return validator - */ - private static SignatureValidator signatureUsesAcceptableHashAlgorithm(Policy policy) { - return new SignatureValidator() { - @Override - public void verify(PGPSignature signature) throws SignatureValidationException { - HashAlgorithm hashAlgorithm = HashAlgorithm.fromId(signature.getHashAlgorithm()); - Policy.HashAlgorithmPolicy hashAlgorithmPolicy = getHashAlgorithmPolicyForSignature(signature, policy); - - if (!hashAlgorithmPolicy.isAcceptable(signature.getHashAlgorithm())) { - throw new SignatureValidationException("Signature uses unacceptable hash algorithm " + hashAlgorithm); - } - } - }; - } - - /** - * Return the applicable {@link Policy.HashAlgorithmPolicy} for the given {@link PGPSignature}. - * Revocation signatures are being policed using a different policy than non-revocation signatures. - * - * @param signature signature - * @param policy revocation policy for revocation sigs, normal policy for non-rev sigs - * @return policy - */ - private static Policy.HashAlgorithmPolicy getHashAlgorithmPolicyForSignature(PGPSignature signature, Policy policy) { - Policy.HashAlgorithmPolicy hashAlgorithmPolicy = null; - SignatureType type = SignatureType.valueOf(signature.getSignatureType()); - if (type == SignatureType.CERTIFICATION_REVOCATION || type == SignatureType.KEY_REVOCATION || type == SignatureType.SUBKEY_REVOCATION) { - hashAlgorithmPolicy = policy.getRevocationSignatureHashAlgorithmPolicy(); - } else { - hashAlgorithmPolicy = policy.getSignatureHashAlgorithmPolicy(); - } - return hashAlgorithmPolicy; - } - - /** - * Verify that a signature does not carry critical unknown notations. - * - * @param registry notation registry of known notations - * @return validator - */ - public static SignatureValidator signatureDoesNotHaveCriticalUnknownNotations(NotationRegistry registry) { - return new SignatureValidator() { - @Override - public void verify(PGPSignature signature) throws SignatureValidationException { - List hashedNotations = SignatureSubpacketsUtil.getHashedNotationData(signature); - for (NotationData notation : hashedNotations) { - if (!notation.isCritical()) { - continue; - } - if (!registry.isKnownNotation(notation.getNotationName())) { - throw new SignatureValidationException("Signature contains unknown critical notation '" + notation.getNotationName() + "' in its hashed area."); - } - } - } - }; - } - - /** - * Verify that a signature does not contain critical unknown subpackets. - * - * @return validator - */ - public static SignatureValidator signatureDoesNotHaveCriticalUnknownSubpackets() { - return new SignatureValidator() { - @Override - public void verify(PGPSignature signature) throws SignatureValidationException { - PGPSignatureSubpacketVector hashedSubpackets = signature.getHashedSubPackets(); - for (int criticalTag : hashedSubpackets.getCriticalTags()) { - try { - SignatureSubpacket.fromCode(criticalTag); - } catch (IllegalArgumentException e) { - throw new SignatureValidationException("Signature contains unknown critical subpacket of type " + Long.toHexString(criticalTag)); - } - } - } - }; - } - - /** - * Verify that a signature is effective right now. - * - * @return validator - */ - public static SignatureValidator signatureIsEffective() { - return signatureIsEffective(new Date()); - } - - /** - * Verify that a signature is effective at the given reference date. - * - * @param validationDate reference date for signature verification - * @return validator - */ - public static SignatureValidator signatureIsEffective(Date validationDate) { - return new SignatureValidator() { - @Override - public void verify(PGPSignature signature) throws SignatureValidationException { - signatureIsAlreadyEffective(validationDate).verify(signature); - signatureIsNotYetExpired(validationDate).verify(signature); - } - }; - } - - /** - * Verify that a signature was created prior to the given reference date. - * - * @param validationDate reference date for signature verification - * @return validator - */ - public static SignatureValidator signatureIsAlreadyEffective(Date validationDate) { - return new SignatureValidator() { - @Override - public void verify(PGPSignature signature) throws SignatureValidationException { - Date signatureCreationTime = SignatureSubpacketsUtil.getSignatureCreationTime(signature).getTime(); - // Hard revocations are always effective - if (SignatureUtils.isHardRevocation(signature)) { - return; - } - - if (signatureCreationTime.after(validationDate)) { - throw new SignatureValidationException("Signature was created at " + signatureCreationTime + " and is therefore not yet valid at " + validationDate); - } - } - }; - } - - /** - * Verify that a signature is not yet expired. - * - * @param validationDate reference date for signature verification - * @return validator - */ - public static SignatureValidator signatureIsNotYetExpired(Date validationDate) { - return new SignatureValidator() { - @Override - public void verify(PGPSignature signature) throws SignatureValidationException { - // Hard revocations do not expire - if (SignatureUtils.isHardRevocation(signature)) { - return; - } - - Date signatureExpirationTime = SignatureSubpacketsUtil.getSignatureExpirationTimeAsDate(signature); - if (signatureExpirationTime != null && signatureExpirationTime.before(validationDate)) { - throw new SignatureValidationException("Signature is already expired (expiration: " + signatureExpirationTime + ", validation: " + validationDate + ")"); - } - } - }; - } - - /** - * Verify that a signature is not malformed. - * A signature is malformed if it has no hashed creation time subpacket, - * it predates the creation time of the signing key, or it predates the creation date - * of the signing key binding signature. - * - * @param creator signing key - * @return validator - */ - public static SignatureValidator signatureIsNotMalformed(PGPPublicKey creator) { - return new SignatureValidator() { - @Override - public void verify(PGPSignature signature) throws SignatureValidationException { - signatureHasHashedCreationTime().verify(signature); - signatureDoesNotPredateSigningKey(creator).verify(signature); - signatureDoesNotPredateSigningKeyBindingDate(creator).verify(signature); - } - }; - } - - /** - * Verify that a signature has a hashed creation time subpacket. - * - * @return validator - */ - public static SignatureValidator signatureHasHashedCreationTime() { - return new SignatureValidator() { - @Override - public void verify(PGPSignature signature) throws SignatureValidationException { - SignatureCreationTime creationTime = SignatureSubpacketsUtil.getSignatureCreationTime(signature); - if (creationTime == null) { - throw new SignatureValidationException("Malformed signature. Signature has no signature creation time subpacket in its hashed area."); - } - } - }; - } - - /** - * Verify that a signature does not predate the creation time of the signing key. - * - * @param key signing key - * @return validator - */ - public static SignatureValidator signatureDoesNotPredateSigningKey(PGPPublicKey key) { - return new SignatureValidator() { - @Override - public void verify(PGPSignature signature) throws SignatureValidationException { - // TODO: Uncommenting the code below would mean that fake issuers would become a problem for sig verification - /* - long keyId = signature.getKeyID(); - if (keyId == 0) { - OpenPgpV4Fingerprint fingerprint = SignatureSubpacketsUtil.getIssuerFingerprintAsOpenPgpV4Fingerprint(signature); - if (fingerprint == null) { - throw new SignatureValidationException("Signature does not contain an issuer-id, neither an issuer-fingerprint subpacket."); - } - keyId = fingerprint.getKeyId(); - } - if (keyId != key.getKeyID()) { - throw new IllegalArgumentException("Signature was not created using key " + Long.toHexString(key.getKeyID())); - } - */ - - Date keyCreationTime = key.getCreationTime(); - Date signatureCreationTime = signature.getCreationTime(); - - if (keyCreationTime.after(signatureCreationTime)) { - throw new SignatureValidationException("Signature predates its signing key (key creation: " + keyCreationTime + ", signature creation: " + signatureCreationTime + ")"); - } - } - }; - } - - /** - * Verify that a signature does not predate the binding date of the signing key. - * - * @param signingKey signing key - * @return validator - */ - public static SignatureValidator signatureDoesNotPredateSigningKeyBindingDate(PGPPublicKey signingKey) { - return new SignatureValidator() { - @Override - public void verify(PGPSignature signature) throws SignatureValidationException { - if (signingKey.isMasterKey()) { - return; - } - boolean predatesBindingSig = true; - Iterator bindingSignatures = signingKey.getSignaturesOfType(SignatureType.SUBKEY_BINDING.getCode()); - if (!bindingSignatures.hasNext()) { - throw new SignatureValidationException("Signing subkey does not have a subkey binding signature."); - } - while (bindingSignatures.hasNext()) { - PGPSignature bindingSig = bindingSignatures.next(); - if (!bindingSig.getCreationTime().after(signature.getCreationTime())) { - predatesBindingSig = false; - } - } - if (predatesBindingSig) { - throw new SignatureValidationException("Signature was created before the signing key was bound to the key ring."); - } - } - }; - } - - /** - * Verify that a subkey binding signature is correct. - * - * @param primaryKey primary key - * @param subkey subkey - * @return validator - */ - public static SignatureValidator correctSubkeyBindingSignature(PGPPublicKey primaryKey, PGPPublicKey subkey) { - return new SignatureValidator() { - @Override - public void verify(PGPSignature signature) throws SignatureValidationException { - if (primaryKey.getKeyID() == subkey.getKeyID()) { - throw new SignatureValidationException("Primary key cannot be its own subkey."); - } - try { - signature.init(ImplementationFactory.getInstance().getPGPContentVerifierBuilderProvider(), primaryKey); - boolean valid = signature.verifyCertification(primaryKey, subkey); - if (!valid) { - throw new SignatureValidationException("Signature is not correct."); - } - } catch (PGPException e) { - throw new SignatureValidationException("Cannot verify subkey binding signature correctness", e); - } - } - }; - } - - /** - * Verify that a primary key binding signature is correct. - * - * @param primaryKey primary key - * @param subkey subkey - * @return validator - */ - public static SignatureValidator correctPrimaryKeyBindingSignature(PGPPublicKey primaryKey, PGPPublicKey subkey) { - return new SignatureValidator() { - @Override - public void verify(PGPSignature signature) throws SignatureValidationException { - try { - signature.init(ImplementationFactory.getInstance().getPGPContentVerifierBuilderProvider(), subkey); - boolean valid = signature.verifyCertification(primaryKey, subkey); - if (!valid) { - throw new SignatureValidationException("Primary Key Binding Signature is not correct."); - } - } catch (PGPException e) { - throw new SignatureValidationException("Cannot verify primary key binding signature correctness", e); - } - } - }; - } - - /** - * Verify that a direct-key signature is correct. - * - * @param signer signing key - * @param signee signed key - * @return validator - */ - public static SignatureValidator correctSignatureOverKey(PGPPublicKey signer, PGPPublicKey signee) { - return new SignatureValidator() { - @Override - public void verify(PGPSignature signature) throws SignatureValidationException { - try { - signature.init(ImplementationFactory.getInstance().getPGPContentVerifierBuilderProvider(), signer); - boolean valid = false; - if (signer.getKeyID() != signee.getKeyID()) { - valid = signature.verifyCertification(signer, signee); - } else { - valid = signature.verifyCertification(signee); - } - if (!valid) { - throw new SignatureValidationException("Signature is not correct."); - } - } catch (PGPException e) { - throw new SignatureValidationException("Cannot verify direct-key signature correctness", e); - } - } - }; - } - - /** - * Verify that a signature is a certification signature. - * - * @return validator - */ - public static SignatureValidator signatureIsCertification() { - return signatureIsOfType( - SignatureType.POSITIVE_CERTIFICATION, - SignatureType.CASUAL_CERTIFICATION, - SignatureType.GENERIC_CERTIFICATION, - SignatureType.NO_CERTIFICATION); - } - - /** - * Verify that a signature type equals one of the given {@link SignatureType SignatureTypes}. - * - * @param signatureTypes one or more signature types - * @return validator - */ - public static SignatureValidator signatureIsOfType(SignatureType... signatureTypes) { - return new SignatureValidator() { - @Override - public void verify(PGPSignature signature) throws SignatureValidationException { - SignatureType type = SignatureType.valueOf(signature.getSignatureType()); - boolean valid = false; - for (SignatureType allowed : signatureTypes) { - if (type == allowed) { - valid = true; - break; - } - } - if (!valid) { - throw new SignatureValidationException("Signature is of type " + type + " while only " + Arrays.toString(signatureTypes) + " are allowed here."); - } - } - }; - } - - /** - * Verify that a signature over a user-id is correct. - * - * @param userId user-id - * @param certifiedKey key carrying the user-id - * @param certifyingKey key that created the signature. - * @return validator - */ - public static SignatureValidator correctSignatureOverUserId(String userId, PGPPublicKey certifiedKey, PGPPublicKey certifyingKey) { - return new SignatureValidator() { - @Override - public void verify(PGPSignature signature) throws SignatureValidationException { - try { - signature.init(ImplementationFactory.getInstance().getPGPContentVerifierBuilderProvider(), certifyingKey); - boolean valid = signature.verifyCertification(userId, certifiedKey); - if (!valid) { - throw new SignatureValidationException("Signature over user-id '" + userId + "' is not correct."); - } - } catch (PGPException e) { - throw new SignatureValidationException("Cannot verify signature over user-id '" + userId + "'.", e); - } - } - }; - } - - /** - * Verify that a signature over a user-attribute packet is correct. - * - * @param userAttributes user attributes - * @param certifiedKey key carrying the user-attributes - * @param certifyingKey key that created the certification signature - * @return validator - */ - public static SignatureValidator correctSignatureOverUserAttributes(PGPUserAttributeSubpacketVector userAttributes, PGPPublicKey certifiedKey, PGPPublicKey certifyingKey) { - return new SignatureValidator() { - @Override - public void verify(PGPSignature signature) throws SignatureValidationException { - try { - signature.init(ImplementationFactory.getInstance().getPGPContentVerifierBuilderProvider(), certifyingKey); - boolean valid = signature.verifyCertification(userAttributes, certifiedKey); - if (!valid) { - throw new SignatureValidationException("Signature over user-attribute vector is not correct."); - } - } catch (PGPException e) { - throw new SignatureValidationException("Cannot verify signature over user-attribute vector.", e); - } - } - }; - } - - public static SignatureValidator signatureWasCreatedInBounds(Date notBefore, Date notAfter) { - return new SignatureValidator() { - @Override - public void verify(PGPSignature signature) throws SignatureValidationException { - Date timestamp = signature.getCreationTime(); - if (notBefore != null && timestamp.before(notBefore)) { - throw new SignatureValidationException("Signature was made before the earliest allowed signature creation time. Created: " + - DateUtil.formatUTCDate(timestamp) + " Earliest allowed: " + DateUtil.formatUTCDate(notBefore)); - } - if (notAfter != null && timestamp.after(notAfter)) { - throw new SignatureValidationException("Signature was made after the latest allowed signature creation time. Created: " + - DateUtil.formatUTCDate(timestamp) + " Latest allowed: " + DateUtil.formatUTCDate(notAfter)); - } - } - }; - } - -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureValidityComparator.java b/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureValidityComparator.java deleted file mode 100644 index 31d29111..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureValidityComparator.java +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature; - -import java.util.Comparator; - -import org.bouncycastle.openpgp.PGPSignature; - -/** - * Comparator which sorts signatures based on an ordering and on revocation hardness. - * - * If a list of signatures gets ordered using this comparator, hard revocations will always - * come first. - * Further, signatures are ordered by date according to the {@link org.pgpainless.signature.SignatureCreationDateComparator.Order}. - */ -public class SignatureValidityComparator implements Comparator { - - private final SignatureCreationDateComparator creationDateComparator; - - /** - * Create a new {@link SignatureValidityComparator} which orders signatures oldest first. - * Still, hard revocations will come first. - */ - public SignatureValidityComparator() { - this(SignatureCreationDateComparator.DEFAULT_ORDER); - } - - /** - * Create a new {@link SignatureValidityComparator} which orders signatures following the passed ordering. - * Still, hard revocations will come first. - */ - public SignatureValidityComparator(SignatureCreationDateComparator.Order order) { - this.creationDateComparator = new SignatureCreationDateComparator(order); - } - - @Override - public int compare(PGPSignature one, PGPSignature two) { - boolean oneIsHard = SignatureUtils.isHardRevocation(one); - boolean twoIsHard = SignatureUtils.isHardRevocation(two); - - // both have same "hardness", so compare creation time - if (oneIsHard == twoIsHard) { - return creationDateComparator.compare(one, two); - } - // favor the "harder" signature - return oneIsHard ? -1 : 1; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureVerifier.java b/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureVerifier.java deleted file mode 100644 index 30ec5d0c..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureVerifier.java +++ /dev/null @@ -1,456 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature; - -import java.io.IOException; -import java.io.InputStream; -import java.util.Date; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.PGPUserAttributeSubpacketVector; -import org.pgpainless.algorithm.SignatureType; -import org.pgpainless.exception.SignatureValidationException; -import org.pgpainless.implementation.ImplementationFactory; -import org.pgpainless.policy.Policy; - -/** - * Collection of static methods for signature verification. - * Signature verification entails validation of certain criteria (see {@link SignatureValidator}), as well as - * cryptographic verification of signature correctness. - */ -public final class SignatureVerifier { - - private SignatureVerifier() { - - } - - /** - * Verify a signature (certification or revocation) over a user-id. - * - * @param userId user-id - * @param signature certification signature - * @param signingKey key that created the certification - * @param keyWithUserId key carrying the user-id - * @param policy policy - * @param validationDate reference date for signature verification - * @return true if signature verification is successful - * - * @throws SignatureValidationException if signature verification fails for some reason - */ - public static boolean verifySignatureOverUserId(String userId, PGPSignature signature, PGPPublicKey signingKey, PGPPublicKey keyWithUserId, Policy policy, Date validationDate) - throws SignatureValidationException { - SignatureType type = SignatureType.valueOf(signature.getSignatureType()); - switch (type) { - case GENERIC_CERTIFICATION: - case NO_CERTIFICATION: - case CASUAL_CERTIFICATION: - case POSITIVE_CERTIFICATION: - return verifyUserIdCertification(userId, signature, signingKey, keyWithUserId, policy, validationDate); - case CERTIFICATION_REVOCATION: - return verifyUserIdRevocation(userId, signature, signingKey, keyWithUserId, policy, validationDate); - default: - throw new SignatureValidationException("Signature is not a valid user-id certification/revocation signature: " + type); - } - } - - /** - * Verify a certification self-signature over a user-id. - * - * @param userId user-id - * @param signature certification signature - * @param primaryKey primary key - * @param policy policy - * @param validationDate reference date for signature verification - * @return true if the self-signature is verified successfully - * - * @throws SignatureValidationException if signature verification fails for some reason - */ - public static boolean verifyUserIdCertification(String userId, PGPSignature signature, PGPPublicKey primaryKey, Policy policy, Date validationDate) - throws SignatureValidationException { - return verifyUserIdCertification(userId, signature, primaryKey, primaryKey, policy, validationDate); - } - - /** - * Verify a user-id certification. - * - * @param userId user-id - * @param signature certification signature - * @param signingKey key that created the certification - * @param keyWithUserId primary key that carries the user-id - * @param policy policy - * @param validationDate reference date for signature verification - * @return true if signature verification is successful - * - * @throws SignatureValidationException if signature verification fails for some reason - */ - public static boolean verifyUserIdCertification(String userId, PGPSignature signature, PGPPublicKey signingKey, PGPPublicKey keyWithUserId, Policy policy, Date validationDate) - throws SignatureValidationException { - SignatureValidator.signatureIsCertification().verify(signature); - SignatureValidator.signatureStructureIsAcceptable(signingKey, policy).verify(signature); - SignatureValidator.signatureIsEffective(validationDate).verify(signature); - SignatureValidator.correctSignatureOverUserId(userId, keyWithUserId, signingKey).verify(signature); - - return true; - } - - /** - * Verify a user-id revocation self-signature. - * - * @param userId user-id - * @param signature user-id revocation signature - * @param primaryKey primary key - * @param policy policy - * @param validationDate reference date for signature verification - * @return true if the user-id revocation signature is successfully verified - * - * @throws SignatureValidationException if signature verification fails for some reason - */ - public static boolean verifyUserIdRevocation(String userId, PGPSignature signature, PGPPublicKey primaryKey, Policy policy, Date validationDate) - throws SignatureValidationException { - return verifyUserIdRevocation(userId, signature, primaryKey, primaryKey, policy, validationDate); - } - - /** - * Verify a user-id revocation signature. - * - * @param userId user-id - * @param signature revocation signature - * @param signingKey key that created the revocation signature - * @param keyWithUserId primary key carrying the user-id - * @param policy policy - * @param validationDate reference date for signature verification - * @return true if the user-id revocation signature is successfully verified - * - * @throws SignatureValidationException if signature verification fails for some reason - */ - public static boolean verifyUserIdRevocation(String userId, PGPSignature signature, PGPPublicKey signingKey, PGPPublicKey keyWithUserId, Policy policy, Date validationDate) - throws SignatureValidationException { - SignatureValidator.signatureIsOfType(SignatureType.CERTIFICATION_REVOCATION).verify(signature); - SignatureValidator.signatureStructureIsAcceptable(signingKey, policy).verify(signature); - SignatureValidator.signatureIsEffective(validationDate).verify(signature); - SignatureValidator.correctSignatureOverUserId(userId, keyWithUserId, signingKey).verify(signature); - - return true; - } - - /** - * Verify a certification self-signature over a user-attributes packet. - * - * @param userAttributes user attributes - * @param signature certification self-signature - * @param primaryKey primary key that carries the user-attributes - * @param policy policy - * @param validationDate reference date for signature verification - * @return true if the signature can be verified successfully - * - * @throws SignatureValidationException if signature verification fails for some reason - */ - public static boolean verifyUserAttributesCertification(PGPUserAttributeSubpacketVector userAttributes, - PGPSignature signature, PGPPublicKey primaryKey, - Policy policy, Date validationDate) - throws SignatureValidationException { - return verifyUserAttributesCertification(userAttributes, signature, primaryKey, primaryKey, policy, validationDate); - } - - /** - * Verify a certification signature over a user-attributes packet. - * - * @param userAttributes user attributes - * @param signature certification signature - * @param signingKey key that created the user-attributes certification - * @param keyWithUserAttributes key that carries the user-attributes certification - * @param policy policy - * @param validationDate reference date for signature verification - * @return true if the signature can be verified successfully - * - * @throws SignatureValidationException if signature verification fails for some reason - */ - public static boolean verifyUserAttributesCertification(PGPUserAttributeSubpacketVector userAttributes, - PGPSignature signature, PGPPublicKey signingKey, - PGPPublicKey keyWithUserAttributes, Policy policy, - Date validationDate) - throws SignatureValidationException { - SignatureValidator.signatureIsCertification().verify(signature); - SignatureValidator.signatureStructureIsAcceptable(signingKey, policy).verify(signature); - SignatureValidator.signatureIsEffective(validationDate).verify(signature); - SignatureValidator.correctSignatureOverUserAttributes(userAttributes, keyWithUserAttributes, signingKey).verify(signature); - - return true; - } - - /** - * Verify a user-attributes revocation self-signature. - * - * @param userAttributes user-attributes - * @param signature user-attributes revocation signature - * @param primaryKey primary key that carries the user-attributes - * @param policy policy - * @param validationDate reference date for signature verification - * @return true if the revocation signature can be verified successfully - * - * @throws SignatureValidationException if signature verification fails for some reason - */ - public static boolean verifyUserAttributesRevocation(PGPUserAttributeSubpacketVector userAttributes, - PGPSignature signature, PGPPublicKey primaryKey, - Policy policy, Date validationDate) - throws SignatureValidationException { - return verifyUserAttributesRevocation(userAttributes, signature, primaryKey, primaryKey, policy, validationDate); - } - - /** - * Verify a user-attributes revocation signature. - * - * @param userAttributes user-attributes - * @param signature revocation signature - * @param signingKey revocation key - * @param keyWithUserAttributes key that carries the user-attributes - * @param policy policy - * @param validationDate reference date for signature verification - * @return true if the revocation signature can be verified successfully - * - * @throws SignatureValidationException if signature verification fails for some reason - */ - public static boolean verifyUserAttributesRevocation(PGPUserAttributeSubpacketVector userAttributes, - PGPSignature signature, PGPPublicKey signingKey, - PGPPublicKey keyWithUserAttributes, Policy policy, - Date validationDate) - throws SignatureValidationException { - SignatureValidator.signatureIsOfType(SignatureType.CERTIFICATION_REVOCATION).verify(signature); - SignatureValidator.signatureStructureIsAcceptable(signingKey, policy).verify(signature); - SignatureValidator.signatureIsEffective(validationDate).verify(signature); - SignatureValidator.correctSignatureOverUserAttributes(userAttributes, keyWithUserAttributes, signingKey).verify(signature); - - return true; - } - - /** - * Verify a subkey binding signature. - * - * @param signature binding signature - * @param primaryKey primary key - * @param subkey subkey - * @param policy policy - * @param validationDate reference date for signature verification - * @return true if the binding signature can be verified successfully - * - * @throws SignatureValidationException if signature verification fails for some reason - */ - public static boolean verifySubkeyBindingSignature(PGPSignature signature, PGPPublicKey primaryKey, PGPPublicKey subkey, Policy policy, Date validationDate) - throws SignatureValidationException { - SignatureValidator.signatureIsOfType(SignatureType.SUBKEY_BINDING).verify(signature); - SignatureValidator.signatureStructureIsAcceptable(primaryKey, policy).verify(signature); - SignatureValidator.signatureIsEffective(validationDate).verify(signature); - SignatureValidator.hasValidPrimaryKeyBindingSignatureIfRequired(primaryKey, subkey, policy, validationDate).verify(signature); - SignatureValidator.correctSubkeyBindingSignature(primaryKey, subkey).verify(signature); - - return true; - } - - /** - * Verify a subkey revocation signature. - * - * @param signature subkey revocation signature - * @param primaryKey primary key - * @param subkey subkey - * @param policy policy - * @param validationDate reference date for signature verification - * @return true if the subkey revocation signature can be verified successfully - * - * @throws SignatureValidationException if signature verification fails for some reason - */ - public static boolean verifySubkeyBindingRevocation(PGPSignature signature, PGPPublicKey primaryKey, PGPPublicKey subkey, Policy policy, Date validationDate) throws SignatureValidationException { - SignatureValidator.signatureIsOfType(SignatureType.SUBKEY_REVOCATION).verify(signature); - SignatureValidator.signatureStructureIsAcceptable(primaryKey, policy).verify(signature); - SignatureValidator.signatureIsEffective(validationDate).verify(signature); - SignatureValidator.correctSignatureOverKey(primaryKey, subkey).verify(signature); - - return true; - } - - /** - * Verify a direct-key self-signature. - * - * @param signature signature - * @param primaryKey primary key - * @param policy policy - * @param validationDate reference date for signature verification - * @return true if the signature can be verified successfully - * - * @throws SignatureValidationException if signature verification fails for some reason - */ - public static boolean verifyDirectKeySignature(PGPSignature signature, PGPPublicKey primaryKey, Policy policy, Date validationDate) - throws SignatureValidationException { - return verifyDirectKeySignature(signature, primaryKey, primaryKey, policy, validationDate); - } - - /** - * Verify a direct-key signature. - * - * @param signature signature - * @param signingKey signing key - * @param signedKey signed key - * @param policy policy - * @param validationDate reference date for signature verification - * @return true if signature verification is successful - * - * @throws SignatureValidationException if signature verification fails for some reason - */ - public static boolean verifyDirectKeySignature(PGPSignature signature, PGPPublicKey signingKey, PGPPublicKey signedKey, Policy policy, Date validationDate) - throws SignatureValidationException { - SignatureValidator.signatureIsOfType(SignatureType.DIRECT_KEY).verify(signature); - SignatureValidator.signatureStructureIsAcceptable(signingKey, policy).verify(signature); - SignatureValidator.signatureIsEffective(validationDate).verify(signature); - SignatureValidator.correctSignatureOverKey(signingKey, signedKey).verify(signature); - - return true; - } - - /** - * Verify a key revocation signature. - * - * @param signature signature - * @param primaryKey primary key - * @param policy policy - * @param validationDate reference date for signature verification - * @return true if signature verification is successful - * - * @throws SignatureValidationException if signature verification fails for some reason - */ - public static boolean verifyKeyRevocationSignature(PGPSignature signature, PGPPublicKey primaryKey, Policy policy, Date validationDate) - throws SignatureValidationException { - SignatureValidator.signatureIsOfType(SignatureType.KEY_REVOCATION).verify(signature); - SignatureValidator.signatureStructureIsAcceptable(primaryKey, policy).verify(signature); - SignatureValidator.signatureIsEffective(validationDate).verify(signature); - SignatureValidator.correctSignatureOverKey(primaryKey, primaryKey).verify(signature); - - return true; - } - - /** - * Initialize a signature and verify it afterwards by updating it with the signed data. - * - * @param signature OpenPGP signature - * @param signedData input stream containing the signed data - * @param signingKey the key that created the signature - * @param policy policy - * @param validationDate reference date of signature verification - * @return true if the signature is successfully verified - * - * @throws SignatureValidationException if the signature verification fails for some reason - */ - public static boolean verifyUninitializedSignature(PGPSignature signature, InputStream signedData, PGPPublicKey signingKey, Policy policy, Date validationDate) throws SignatureValidationException { - initializeSignatureAndUpdateWithSignedData(signature, signedData, signingKey); - return verifyInitializedSignature(signature, signingKey, policy, validationDate); - } - - /** - * Initialize a signature and then update it with the signed data from the given {@link InputStream}. - * - * @param signature OpenPGP signature - * @param signedData input stream containing signed data - * @param signingKey key that created the signature - * - * @throws SignatureValidationException in case the signature cannot be verified for some reason - */ - public static void initializeSignatureAndUpdateWithSignedData(PGPSignature signature, InputStream signedData, PGPPublicKey signingKey) - throws SignatureValidationException { - try { - signature.init(ImplementationFactory.getInstance().getPGPContentVerifierBuilderProvider(), signingKey); - int read; - byte[] buf = new byte[8192]; - byte lastByte = -1; - while ((read = signedData.read(buf)) != -1) { - // If we previously omitted a newline, but the stream is not yet empty, add it now - if (lastByte == (byte) '\n') { - signature.update(lastByte); - } - lastByte = buf[read - 1]; - - if (lastByte == (byte) '\n') { - // if last byte in buffer is newline, omit it for now - signature.update(buf, 0, read - 1); - } else { - // otherwise, write buffer as usual - signature.update(buf, 0, read); - } - } - } catch (PGPException e) { - throw new SignatureValidationException("Cannot init signature.", e); - } catch (IOException e) { - throw new SignatureValidationException("Cannot update signature.", e); - } - } - - /** - * Verify an initialized signature. - * An initialized signature was already updated with the signed data. - * - * @param signature OpenPGP signature - * @param signingKey key that created the signature - * @param policy policy - * @param validationDate reference date for signature verification - * @return true if signature is verified successfully - * - * @throws SignatureValidationException if signature verification fails for some reason - */ - public static boolean verifyInitializedSignature(PGPSignature signature, PGPPublicKey signingKey, Policy policy, Date validationDate) - throws SignatureValidationException { - SignatureValidator.wasPossiblyMadeByKey(signingKey).verify(signature); - SignatureValidator.signatureStructureIsAcceptable(signingKey, policy).verify(signature); - SignatureValidator.signatureIsEffective(validationDate).verify(signature); - - try { - if (!signature.verify()) { - throw new SignatureValidationException("Signature is not correct."); - } - return true; - } catch (PGPException e) { - throw new SignatureValidationException("Could not verify signature correctness.", e); - } - } - - public static boolean verifyOnePassSignature(PGPSignature signature, PGPPublicKey signingKey, OnePassSignatureCheck onePassSignature, Policy policy) - throws SignatureValidationException { - try { - SignatureValidator.wasPossiblyMadeByKey(signingKey).verify(signature); - SignatureValidator.signatureStructureIsAcceptable(signingKey, policy).verify(signature); - SignatureValidator.signatureIsEffective().verify(signature); - } catch (SignatureValidationException e) { - throw new SignatureValidationException("Signature is not valid: " + e.getMessage(), e); - } - - try { - if (onePassSignature.getSignature() == null) { - throw new IllegalStateException("No comparison signature provided."); - } - if (!onePassSignature.getOnePassSignature().verify(signature)) { - throw new SignatureValidationException("Bad signature of key " + Long.toHexString(signingKey.getKeyID())); - } - } catch (PGPException e) { - throw new SignatureValidationException("Could not verify correctness of One-Pass-Signature: " + e.getMessage(), e); - } - - return true; - } - - /** - * Verify a signature (certification or revocation) over a user-id. - * - * @param userId user-id - * @param signature self-signature - * @param primaryKey primary key that created the signature - * @param policy policy - * @param validationDate reference date for signature verification - * @return true if the signature is successfully verified - * - * @throws SignatureValidationException if signature verification fails for some reason - */ - public static boolean verifySignatureOverUserId(String userId, PGPSignature signature, PGPPublicKey primaryKey, Policy policy, Date validationDate) - throws SignatureValidationException { - return verifySignatureOverUserId(userId, signature, primaryKey, primaryKey, policy, validationDate); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/signature/package-info.java deleted file mode 100644 index 624a2e24..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Classes related to OpenPGP signatures. - */ -package org.pgpainless.signature; diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketGeneratorUtil.java b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketGeneratorUtil.java deleted file mode 100644 index 948441f3..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketGeneratorUtil.java +++ /dev/null @@ -1,135 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature.subpackets; - -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import javax.annotation.Nonnull; - -import org.bouncycastle.bcpg.SignatureSubpacket; -import org.bouncycastle.bcpg.SignatureSubpacketTags; -import org.bouncycastle.bcpg.sig.KeyFlags; -import org.bouncycastle.openpgp.PGPSignatureSubpacketGenerator; -import org.pgpainless.algorithm.KeyFlag; - -/** - * Utility class that helps dealing with BCs SignatureSubpacketGenerator class. - */ -public final class SignatureSubpacketGeneratorUtil { - - private SignatureSubpacketGeneratorUtil() { - - } - - /** - * Return a list of {@link SignatureSubpacket SignatureSubpackets} from the subpacket generator, which correspond - * to the given {@link org.pgpainless.algorithm.SignatureSubpacket} type. - * - * @param type subpacket type - * @param generator subpacket generator - * @param

generic subpacket type - * @return possibly empty list of subpackets - */ - public static

List

getSubpacketsOfType(org.pgpainless.algorithm.SignatureSubpacket type, - PGPSignatureSubpacketGenerator generator) { - SignatureSubpacket[] subpackets = generator.getSubpackets(type.getCode()); - List

list = new ArrayList<>(); - for (SignatureSubpacket p : subpackets) { - list.add((P) p); - } - return list; - } - - /** - * Remove all packets of the given type from the {@link PGPSignatureSubpacketGenerator PGPSignatureSubpacketGenerators} - * internal set. - * - * @param subpacketType type of subpacket to remove - * @param subpacketGenerator subpacket generator - */ - public static void removeAllPacketsOfType(org.pgpainless.algorithm.SignatureSubpacket subpacketType, - PGPSignatureSubpacketGenerator subpacketGenerator) { - removeAllPacketsOfType(subpacketType.getCode(), subpacketGenerator); - } - - /** - * Remove all packets of the given type from the {@link PGPSignatureSubpacketGenerator PGPSignatureSubpacketGenerators} - * internal set. - * - * @param type type of subpacket to remove - * @param subpacketGenerator subpacket generator - */ - public static void removeAllPacketsOfType(int type, PGPSignatureSubpacketGenerator subpacketGenerator) { - for (SignatureSubpacket subpacket : subpacketGenerator.getSubpackets(type)) { - subpacketGenerator.removePacket(subpacket); - } - } - - /** - * Replace all occurrences of a signature creation time subpackets in the subpacket generator - * with a single new instance representing the provided date. - * - * @param date signature creation time - * @param subpacketGenerator subpacket generator - */ - public static void setSignatureCreationTimeInSubpacketGenerator(Date date, PGPSignatureSubpacketGenerator subpacketGenerator) { - removeAllPacketsOfType(SignatureSubpacketTags.CREATION_TIME, subpacketGenerator); - subpacketGenerator.setSignatureCreationTime(false, date); - } - - /** - * Replace all occurrences of key expiration time subpackets in the subpacket generator - * with a single instance representing the new expiration time. - * - * @param expirationDate expiration time as date or null for no expiration - * @param creationDate date on which the key was created - * @param subpacketGenerator subpacket generator - */ - public static void setExpirationDateInSubpacketGenerator(Date expirationDate, - @Nonnull Date creationDate, - PGPSignatureSubpacketGenerator subpacketGenerator) { - removeAllPacketsOfType(SignatureSubpacketTags.KEY_EXPIRE_TIME, subpacketGenerator); - long secondsToExpire = getKeyLifetimeInSeconds(expirationDate, creationDate); - subpacketGenerator.setKeyExpirationTime(true, secondsToExpire); - } - - /** - * Calculate the duration in seconds until the key expires after creation. - * - * @param expirationDate new expiration date - * @param creationTime key creation time - * @return life time of the key in seconds - */ - private static long getKeyLifetimeInSeconds(Date expirationDate, @Nonnull Date creationTime) { - long secondsToExpire = 0; // 0 means "no expiration" - if (expirationDate != null) { - if (creationTime.after(expirationDate)) { - throw new IllegalArgumentException("Key MUST NOT expire before being created. (creation: " + creationTime + ", expiration: " + expirationDate + ")"); - } - secondsToExpire = (expirationDate.getTime() - creationTime.getTime()) / 1000; - } - return secondsToExpire; - } - - /** - * Return true, if the subpacket generator has a {@link KeyFlags} subpacket which carries the given key flag. - * Returns false, if no {@link KeyFlags} subpacket is present. - * If there are more than one instance of a {@link KeyFlags} packet present, only the last occurrence will - * be tested. - * - * @param keyFlag flag to test for - * @param generator subpackets generator - * @return true if the generator has the given key flag set - */ - public static boolean hasKeyFlag(KeyFlag keyFlag, PGPSignatureSubpacketGenerator generator) { - List keyFlagPackets = getSubpacketsOfType(org.pgpainless.algorithm.SignatureSubpacket.keyFlags, generator); - if (keyFlagPackets.isEmpty()) { - return false; - } - KeyFlags last = keyFlagPackets.get(keyFlagPackets.size() - 1); - return KeyFlag.hasKeyFlag(last.getFlags(), keyFlag); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.java b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.java deleted file mode 100644 index 226be119..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.java +++ /dev/null @@ -1,561 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature.subpackets; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Date; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Set; - -import javax.annotation.Nullable; - -import org.bouncycastle.bcpg.sig.Exportable; -import org.bouncycastle.bcpg.sig.Features; -import org.bouncycastle.bcpg.sig.IntendedRecipientFingerprint; -import org.bouncycastle.bcpg.sig.IssuerFingerprint; -import org.bouncycastle.bcpg.sig.IssuerKeyID; -import org.bouncycastle.bcpg.sig.KeyExpirationTime; -import org.bouncycastle.bcpg.sig.KeyFlags; -import org.bouncycastle.bcpg.sig.NotationData; -import org.bouncycastle.bcpg.sig.PreferredAlgorithms; -import org.bouncycastle.bcpg.sig.PrimaryUserID; -import org.bouncycastle.bcpg.sig.Revocable; -import org.bouncycastle.bcpg.sig.RevocationKey; -import org.bouncycastle.bcpg.sig.RevocationReason; -import org.bouncycastle.bcpg.sig.SignatureCreationTime; -import org.bouncycastle.bcpg.sig.SignatureExpirationTime; -import org.bouncycastle.bcpg.sig.SignatureTarget; -import org.bouncycastle.bcpg.sig.SignerUserID; -import org.bouncycastle.bcpg.sig.TrustSignature; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.PGPSignatureList; -import org.bouncycastle.openpgp.PGPSignatureSubpacketVector; -import org.bouncycastle.util.encoders.Hex; -import org.pgpainless.algorithm.CompressionAlgorithm; -import org.pgpainless.algorithm.Feature; -import org.pgpainless.algorithm.HashAlgorithm; -import org.pgpainless.algorithm.KeyFlag; -import org.pgpainless.algorithm.SignatureSubpacket; -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; -import org.pgpainless.key.OpenPgpV4Fingerprint; -import org.pgpainless.signature.SignatureUtils; - -/** - * Utility class to access signature subpackets from signatures. - * - * Since rfc4880 is not always clear about where a signature subpacket can be located (hashed/unhashed area), - * this class makes some educated guesses as to where the subpacket may be found when necessary. - */ -public final class SignatureSubpacketsUtil { - - private SignatureSubpacketsUtil() { - - } - - /** - * Return the issuer-fingerprint subpacket of the signature. - * Since this packet is self-authenticating, we expect it to be in the unhashed area, - * however as it cannot hurt we search for it in the hashed area first. - * - * @param signature signature - * @return issuer fingerprint or null - */ - public static IssuerFingerprint getIssuerFingerprint(PGPSignature signature) { - return hashedOrUnhashed(signature, SignatureSubpacket.issuerFingerprint); - } - - /** - * Return the {@link IssuerFingerprint} subpacket of the signature into a {@link OpenPgpV4Fingerprint}. - * If no v4 issuer fingerprint is present in the signature, return null. - * - * @param signature signature - * @return v4 fingerprint of the issuer, or null - */ - public static OpenPgpV4Fingerprint getIssuerFingerprintAsOpenPgpV4Fingerprint(PGPSignature signature) { - IssuerFingerprint subpacket = getIssuerFingerprint(signature); - if (subpacket == null) { - return null; - } - - if (subpacket.getKeyVersion() == 4) { - OpenPgpV4Fingerprint fingerprint = new OpenPgpV4Fingerprint(Hex.encode(subpacket.getFingerprint())); - return fingerprint; - } - return null; - } - - /** - * Return the issuer key-id subpacket of the signature. - * Since this packet is self-authenticating, we expect it to be in the unhashed area, - * however as it cannot hurt we search for it in the hashed area first. - * - * @param signature signature - * @return issuer key-id or null - */ - public static IssuerKeyID getIssuerKeyId(PGPSignature signature) { - return hashedOrUnhashed(signature, SignatureSubpacket.issuerKeyId); - } - - /** - * Inspect the given signature's {@link IssuerKeyID} packet to determine the issuer key-id. - * If no such packet is present, return null. - * - * @param signature signature - * @return issuer key-id as {@link Long} - */ - public static Long getIssuerKeyIdAsLong(PGPSignature signature) { - IssuerKeyID keyID = getIssuerKeyId(signature); - if (keyID == null) { - return null; - } - return keyID.getKeyID(); - } - - /** - * Return the revocation reason subpacket of the signature. - * Since this packet is rather important for revocations, we only search for it in the - * hashed area of the signature. - * - * @param signature signature - * @return revocation reason - */ - public static RevocationReason getRevocationReason(PGPSignature signature) { - return hashed(signature, SignatureSubpacket.revocationReason); - } - - /** - * Return the signature creation time subpacket. - * Since this packet is rather important, we only search for it in the hashed area - * of the signature. - * - * @param signature signature - * @return signature creation time subpacket - */ - public static SignatureCreationTime getSignatureCreationTime(PGPSignature signature) { - return hashed(signature, SignatureSubpacket.signatureCreationTime); - } - - /** - * Return the signature expiration time subpacket of the signature. - * Since this packet is rather important, we only search for it in the hashed area of the signature. - * - * @param signature signature - * @return signature expiration time - */ - public static SignatureExpirationTime getSignatureExpirationTime(PGPSignature signature) { - return hashed(signature, SignatureSubpacket.signatureExpirationTime); - } - - /** - * Return the signatures expiration time as a date. - * The expiration date is computed by adding the expiration time to the signature creation date. - * If the signature has no expiration time subpacket, or the expiration time is set to '0', this message returns null. - * - * @param signature signature - * @return expiration time as date - */ - public static Date getSignatureExpirationTimeAsDate(PGPSignature signature) { - SignatureExpirationTime subpacket = getSignatureExpirationTime(signature); - if (subpacket == null) { - return null; - } - return SignatureUtils.datePlusSeconds(signature.getCreationTime(), subpacket.getTime()); - } - - /** - * Return the key expiration time subpacket of this signature. - * We only look for it in the hashed area of the signature. - * - * @param signature signature - * @return key expiration time - */ - public static KeyExpirationTime getKeyExpirationTime(PGPSignature signature) { - return hashed(signature, SignatureSubpacket.keyExpirationTime); - } - - /** - * Return the signatures key-expiration time as a date. - * The expiration date is computed by adding the signatures' key-expiration time to the signing keys - * creation date. - * If the signature does not have a key-expiration time subpacket, or its value is '0', this method returns null. - * - * @param signature self-signature carrying the key-expiration time subpacket - * @param signingKey signature creation key - * @return key expiration time as date - */ - public static Date getKeyExpirationTimeAsDate(PGPSignature signature, PGPPublicKey signingKey) { - KeyExpirationTime subpacket = getKeyExpirationTime(signature); - if (subpacket == null) { - return null; - } - if (signature.getKeyID() != signingKey.getKeyID()) { - throw new IllegalArgumentException("Provided key (" + Long.toHexString(signingKey.getKeyID()) + ") did not create the signature (" + Long.toHexString(signature.getKeyID()) + ")"); - } - return SignatureUtils.datePlusSeconds(signingKey.getCreationTime(), subpacket.getTime()); - } - - /** - * Return the revocable subpacket of this signature. - * We only look for it in the hashed area of the signature. - * - * @param signature signature - * @return revocable subpacket - */ - public static Revocable getRevocable(PGPSignature signature) { - return hashed(signature, SignatureSubpacket.revocable); - } - - /** - * Return the symmetric algorithm preferences from the signatures hashed area. - * - * @param signature signature - * @return symm. algo. prefs - */ - public static PreferredAlgorithms getPreferredSymmetricAlgorithms(PGPSignature signature) { - return hashed(signature, SignatureSubpacket.preferredSymmetricAlgorithms); - } - - /** - * Return the preferred {@link SymmetricKeyAlgorithm SymmetricKeyAlgorithms} as present in the signature. - * If no preference is given with regard to symmetric encryption algorithms, return an empty set. - * - * In any case, the resulting set is ordered by occurrence ({@link LinkedHashSet}). - * @param signature signature - * @return ordered set of symmetric key algorithm preferences - */ - public static Set parsePreferredSymmetricKeyAlgorithms(PGPSignature signature) { - Set algorithms = new LinkedHashSet<>(); - PreferredAlgorithms preferences = getPreferredSymmetricAlgorithms(signature); - if (preferences != null) { - for (int code : preferences.getPreferences()) { - algorithms.add(SymmetricKeyAlgorithm.fromId(code)); - } - } - return algorithms; - } - - /** - * Return the hash algorithm preferences from the signatures hashed area. - * - * @param signature signature - * @return hash algo prefs - */ - public static PreferredAlgorithms getPreferredHashAlgorithms(PGPSignature signature) { - return hashed(signature, SignatureSubpacket.preferredHashAlgorithms); - } - - /** - * Return the preferred {@link HashAlgorithm HashAlgorithms} as present in the signature. - * If no preference is given with regard to hash algorithms, return an empty set. - * - * In any case, the resulting set is ordered by occurrence ({@link LinkedHashSet}). - * @param signature signature - * @return ordered set of hash algorithm preferences - */ - public static Set parsePreferredHashAlgorithms(PGPSignature signature) { - Set algorithms = new LinkedHashSet<>(); - PreferredAlgorithms preferences = getPreferredHashAlgorithms(signature); - if (preferences != null) { - for (int code : preferences.getPreferences()) { - algorithms.add(HashAlgorithm.fromId(code)); - } - } - return algorithms; - } - - /** - * Return the compression algorithm preferences from the signatures hashed area. - * - * @param signature signature - * @return compression algo prefs - */ - public static PreferredAlgorithms getPreferredCompressionAlgorithms(PGPSignature signature) { - return hashed(signature, SignatureSubpacket.preferredCompressionAlgorithms); - } - - /** - * Return the preferred {@link CompressionAlgorithm CompressionAlgorithms} as present in the signature. - * If no preference is given with regard to compression algorithms, return an empty set. - * - * In any case, the resulting set is ordered by occurrence ({@link LinkedHashSet}). - * @param signature signature - * @return ordered set of compression algorithm preferences - */ - public static Set parsePreferredCompressionAlgorithms(PGPSignature signature) { - Set algorithms = new LinkedHashSet<>(); - PreferredAlgorithms preferences = getPreferredCompressionAlgorithms(signature); - if (preferences != null) { - for (int code : preferences.getPreferences()) { - algorithms.add(CompressionAlgorithm.fromId(code)); - } - } - return algorithms; - } - - /** - * Return the primary user-id subpacket from the signatures hashed area. - * - * @param signature signature - * @return primary user id - */ - public static PrimaryUserID getPrimaryUserId(PGPSignature signature) { - return hashed(signature, SignatureSubpacket.primaryUserId); - } - - /** - * Return the key flags subpacket from the signatures hashed area. - * - * @param signature signature - * @return key flags - */ - public static KeyFlags getKeyFlags(PGPSignature signature) { - return hashed(signature, SignatureSubpacket.keyFlags); - } - - /** - * Return a list of key flags carried by the signature. - * If the signature is null, or has no {@link KeyFlags} subpacket, return null. - * - * @param signature signature - * @return list of key flags - */ - public static List parseKeyFlags(@Nullable PGPSignature signature) { - if (signature == null) { - return null; - } - KeyFlags keyFlags = getKeyFlags(signature); - if (keyFlags == null) { - return null; - } - return KeyFlag.fromBitmask(keyFlags.getFlags()); - } - - /** - * Return the features subpacket from the signatures hashed area. - * - * @param signature signature - * @return features subpacket - */ - public static Features getFeatures(PGPSignature signature) { - return hashed(signature, SignatureSubpacket.features); - } - - /** - * Parse out the features subpacket of a signature. - * If the signature has no features subpacket, return null. - * Otherwise, return the features as a feature set. - * - * @param signature signature - * @return features as set - */ - public static @Nullable Set parseFeatures(PGPSignature signature) { - Features features = getFeatures(signature); - if (features == null) { - return null; - } - return new LinkedHashSet<>(Feature.fromBitmask(features.getData()[0])); - } - - /** - * Return the signature target subpacket from the signature. - * We search for this subpacket in the hashed and unhashed area (in this order). - * - * @param signature signature - * @return signature target - */ - public static SignatureTarget getSignatureTarget(PGPSignature signature) { - return hashedOrUnhashed(signature, SignatureSubpacket.signatureTarget); - } - - /** - * Return the notation data subpackets from the signatures hashed area. - * - * @param signature signature - * @return hashed notations - */ - public static List getHashedNotationData(PGPSignature signature) { - NotationData[] notations = signature.getHashedSubPackets().getNotationDataOccurrences(); - return Arrays.asList(notations); - } - - /** - * Return a list of all {@link NotationData} objects from the hashed area of the signature that have a - * notation name equal to the given notationName argument. - * - * @param signature signature - * @param notationName notation name - * @return list of matching notation data objects - */ - public static List getHashedNotationData(PGPSignature signature, String notationName) { - List allNotations = getHashedNotationData(signature); - List withName = new ArrayList<>(); - for (NotationData data : allNotations) { - if (data.getNotationName().equals(notationName)) { - withName.add(data); - } - } - return withName; - } - - /** - * Return the notation data subpackets from the signatures unhashed area. - * - * @param signature signture - * @return unhashed notations - */ - public static List getUnhashedNotationData(PGPSignature signature) { - NotationData[] notations = signature.getUnhashedSubPackets().getNotationDataOccurrences(); - return Arrays.asList(notations); - } - - /** - * Return a list of all {@link NotationData} objects from the unhashed area of the signature that have a - * notation name equal to the given notationName argument. - * - * @param signature signature - * @param notationName notation name - * @return list of matching notation data objects - */ - public static List getUnhashedNotationData(PGPSignature signature, String notationName) { - List allNotations = getUnhashedNotationData(signature); - List withName = new ArrayList<>(); - for (NotationData data : allNotations) { - if (data.getNotationName().equals(notationName)) { - withName.add(data); - } - } - return withName; - } - - /** - * Return the revocation key subpacket from the signatures hashed area. - * - * @param signature signature - * @return revocation key - */ - public static RevocationKey getRevocationKey(PGPSignature signature) { - return hashed(signature, SignatureSubpacket.revocationKey); - } - - /** - * Return the signers user-id from the hashed area of the signature. - * TODO: Can this subpacket also be found in the unhashed area? - * - * @param signature signature - * @return signers user-id - */ - public static SignerUserID getSignerUserID(PGPSignature signature) { - return hashed(signature, SignatureSubpacket.signerUserId); - } - - /** - * Return the intended recipients fingerprint subpackets from the hashed area of this signature. - * - * @param signature signature - * @return intended recipient fingerprint subpackets - */ - public static List getIntendedRecipientFingerprints(PGPSignature signature) { - org.bouncycastle.bcpg.SignatureSubpacket[] subpackets = signature.getHashedSubPackets().getSubpackets(SignatureSubpacket.intendedRecipientFingerprint.getCode()); - List intendedRecipients = new ArrayList<>(subpackets.length); - for (org.bouncycastle.bcpg.SignatureSubpacket subpacket : subpackets) { - intendedRecipients.add((IntendedRecipientFingerprint) subpacket); - } - return intendedRecipients; - } - - /** - * Return the embedded signature subpacket from the signatures hashed area. - * - * @param signature signature - * @return embedded signature - */ - public static PGPSignatureList getEmbeddedSignature(PGPSignature signature) throws PGPException { - PGPSignatureList hashed = signature.getHashedSubPackets().getEmbeddedSignatures(); - if (!hashed.isEmpty()) { - return hashed; - } - return signature.getUnhashedSubPackets().getEmbeddedSignatures(); - } - - /** - * Return the signatures exportable certification subpacket from the hashed area. - * - * @param signature signature - * @return exportable certification subpacket - */ - public static Exportable getExportableCertification(PGPSignature signature) { - return hashed(signature, SignatureSubpacket.exportableCertification); - } - - /** - * Return the trust signature packet from the signatures hashed area. - * - * @param signature signature - * @return trust signature subpacket - */ - public static TrustSignature getTrustSignature(PGPSignature signature) { - return hashed(signature, SignatureSubpacket.trustSignature); - } - - /** - * Select a list of all signature subpackets of the given type, which are present in the hashed area of - * the given signature. - * - * @param signature signature - * @param type subpacket type - * @param

generic subpacket type - * @return list of subpackets from the hashed area - */ - private static

P hashed(PGPSignature signature, SignatureSubpacket type) { - return getSignatureSubpacket(signature.getHashedSubPackets(), type); - } - - /** - * Select a list of all signature subpackets of the given type, which are present in the unhashed area of - * the given signature. - * - * @param signature signature - * @param type subpacket type - * @param

generic subpacket type - * @return list of subpackets from the unhashed area - */ - private static

P unhashed(PGPSignature signature, SignatureSubpacket type) { - return getSignatureSubpacket(signature.getUnhashedSubPackets(), type); - } - - /** - * Select a list of all signature subpackets of the given type, which are present in either the hashed - * or the unhashed area of the given signature. - * - * @param signature signature - * @param type subpacket type - * @param

generic subpacket type - * @return list of subpackets from the hashed/unhashed area - */ - private static

P hashedOrUnhashed(PGPSignature signature, SignatureSubpacket type) { - P hashedSubpacket = hashed(signature, type); - return hashedSubpacket != null ? hashedSubpacket : unhashed(signature, type); - } - - /** - * Return the last occurence of a subpacket type in the given signature subpacket vector. - * - * @param vector subpacket vector (hashed/unhashed) - * @param type subpacket type - * @param

generic return type of the subpacket - * @return last occurrence of the subpacket in the vector - */ - public static

P getSignatureSubpacket(PGPSignatureSubpacketVector vector, SignatureSubpacket type) { - org.bouncycastle.bcpg.SignatureSubpacket[] allPackets = vector.getSubpackets(type.getCode()); - if (allPackets.length == 0) { - return null; - } - return (P) allPackets[allPackets.length - 1]; // return last - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/package-info.java deleted file mode 100644 index 09dfd6a2..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Classes related to OpenPGP signatures. - */ -package org.pgpainless.signature.subpackets; diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java b/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java deleted file mode 100644 index f94a4ce3..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java +++ /dev/null @@ -1,232 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.util; - -import java.io.BufferedInputStream; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.regex.Pattern; - -import org.bouncycastle.bcpg.ArmoredInputStream; -import org.bouncycastle.bcpg.ArmoredOutputStream; -import org.bouncycastle.openpgp.PGPKeyRing; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; -import org.bouncycastle.openpgp.PGPUtil; -import org.bouncycastle.openpgp.operator.KeyFingerPrintCalculator; -import org.bouncycastle.util.io.Streams; -import org.pgpainless.algorithm.HashAlgorithm; -import org.pgpainless.key.OpenPgpV4Fingerprint; - -public final class ArmorUtils { - - // MessageIDs are 32 printable characters - private static final Pattern PATTERN_MESSAGE_ID = Pattern.compile("^\\S{32}$"); - - public static final String HEADER_COMMENT = "Comment"; - public static final String HEADER_VERSION = "Version"; - public static final String HEADER_MESSAGEID = "MessageID"; - public static final String HEADER_HASH = "Hash"; - public static final String HEADER_CHARSET = "Charset"; - - private ArmorUtils() { - - } - - public static String toAsciiArmoredString(PGPSecretKeyRing secretKeys) throws IOException { - MultiMap header = keyToHeader(secretKeys); - return toAsciiArmoredString(secretKeys.getEncoded(), header); - } - - public static String toAsciiArmoredString(PGPPublicKeyRing publicKeys) throws IOException { - MultiMap header = keyToHeader(publicKeys); - return toAsciiArmoredString(publicKeys.getEncoded(), header); - } - - public static String toAsciiArmoredString(PGPSecretKeyRingCollection secretKeyRings) throws IOException { - StringBuilder sb = new StringBuilder(); - for (Iterator iterator = secretKeyRings.iterator(); iterator.hasNext(); ) { - PGPSecretKeyRing secretKeyRing = iterator.next(); - sb.append(toAsciiArmoredString(secretKeyRing)); - if (iterator.hasNext()) { - sb.append('\n'); - } - } - return sb.toString(); - } - - public static ArmoredOutputStream toAsciiArmoredStream(PGPKeyRing keyRing, OutputStream outputStream) { - MultiMap header = keyToHeader(keyRing); - return toAsciiArmoredStream(outputStream, header); - } - - public static ArmoredOutputStream toAsciiArmoredStream(OutputStream outputStream, MultiMap header) { - ArmoredOutputStream armoredOutputStream = ArmoredOutputStreamFactory.get(outputStream); - if (header != null) { - for (String headerKey : header.keySet()) { - for (String headerValue : header.get(headerKey)) { - armoredOutputStream.addHeader(headerKey, headerValue); - } - } - } - return armoredOutputStream; - } - - public static String toAsciiArmoredString(PGPPublicKeyRingCollection publicKeyRings) throws IOException { - StringBuilder sb = new StringBuilder(); - for (Iterator iterator = publicKeyRings.iterator(); iterator.hasNext(); ) { - PGPPublicKeyRing publicKeyRing = iterator.next(); - sb.append(toAsciiArmoredString(publicKeyRing)); - if (iterator.hasNext()) { - sb.append('\n'); - } - } - return sb.toString(); - } - - private static MultiMap keyToHeader(PGPKeyRing keyRing) { - MultiMap header = new MultiMap<>(); - OpenPgpV4Fingerprint fingerprint = new OpenPgpV4Fingerprint(keyRing); - Iterator userIds = keyRing.getPublicKey().getUserIDs(); - - header.put(HEADER_COMMENT, fingerprint.prettyPrint()); - if (userIds.hasNext()) { - header.put(HEADER_COMMENT, userIds.next()); - } - return header; - } - - public static String toAsciiArmoredString(byte[] bytes) throws IOException { - return toAsciiArmoredString(bytes, null); - } - - public static String toAsciiArmoredString(byte[] bytes, MultiMap additionalHeaderValues) throws IOException { - return toAsciiArmoredString(new ByteArrayInputStream(bytes), additionalHeaderValues); - } - - public static String toAsciiArmoredString(InputStream inputStream) throws IOException { - return toAsciiArmoredString(inputStream, null); - } - - public static void addHashAlgorithmHeader(ArmoredOutputStream armor, HashAlgorithm hashAlgorithm) { - armor.addHeader(HEADER_HASH, hashAlgorithm.getAlgorithmName()); - } - - public static void addCommentHeader(ArmoredOutputStream armor, String comment) { - armor.addHeader(HEADER_COMMENT, comment); - } - - public static void addMessageIdHeader(ArmoredOutputStream armor, String messageId) { - if (messageId == null) { - throw new NullPointerException("MessageID cannot be null."); - } - if (!PATTERN_MESSAGE_ID.matcher(messageId).matches()) { - throw new IllegalArgumentException("MessageIDs MUST consist of 32 printable characters."); - } - armor.addHeader(HEADER_MESSAGEID, messageId); - } - - public static String toAsciiArmoredString(InputStream inputStream, MultiMap additionalHeaderValues) throws IOException { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - ArmoredOutputStream armor = toAsciiArmoredStream(out, additionalHeaderValues); - Streams.pipeAll(inputStream, armor); - armor.close(); - - return out.toString(); - } - - public static ArmoredOutputStream createArmoredOutputStreamFor(PGPKeyRing keyRing, OutputStream outputStream) { - ArmoredOutputStream armor = ArmoredOutputStreamFactory.get(outputStream); - MultiMap headerMap = keyToHeader(keyRing); - for (String header : headerMap.keySet()) { - for (String value : headerMap.get(header)) { - armor.addHeader(header, value); - } - } - - return armor; - } - - public static List getCommendHeaderValues(ArmoredInputStream armor) { - return getArmorHeaderValues(armor, HEADER_COMMENT); - } - - public static List getMessageIdHeaderValues(ArmoredInputStream armor) { - return getArmorHeaderValues(armor, HEADER_MESSAGEID); - } - - public static List getHashHeaderValues(ArmoredInputStream armor) { - return getArmorHeaderValues(armor, HEADER_HASH); - } - - public static List getHashAlgorithms(ArmoredInputStream armor) { - List algorithmNames = getHashHeaderValues(armor); - List algorithms = new ArrayList<>(); - for (String name : algorithmNames) { - HashAlgorithm algorithm = HashAlgorithm.fromName(name); - if (algorithm != null) { - algorithms.add(algorithm); - } - } - return algorithms; - } - - public static List getVersionHeaderValues(ArmoredInputStream armor) { - return getArmorHeaderValues(armor, HEADER_VERSION); - } - - public static List getCharsetHeaderValues(ArmoredInputStream armor) { - return getArmorHeaderValues(armor, HEADER_CHARSET); - } - - public static List getArmorHeaderValues(ArmoredInputStream armor, String headerKey) { - String[] header = armor.getArmorHeaders(); - String key = headerKey + ": "; - List values = new ArrayList<>(); - for (String line : header) { - if (line.startsWith(key)) { - values.add(line.substring(key.length())); - } - } - return values; - } - - /** - * Hacky workaround for #96. - * For {@link PGPPublicKeyRingCollection#PGPPublicKeyRingCollection(InputStream, KeyFingerPrintCalculator)} - * or {@link PGPSecretKeyRingCollection#PGPSecretKeyRingCollection(InputStream, KeyFingerPrintCalculator)} - * to read all PGPKeyRings properly, we apparently have to make sure that the {@link InputStream} that is given - * as constructor argument is a PGPUtil.BufferedInputStreamExt. - * Since {@link PGPUtil#getDecoderStream(InputStream)} will return an {@link org.bouncycastle.bcpg.ArmoredInputStream} - * if the underlying input stream contains armored data, we have to nest two method calls to make sure that the - * end-result is a PGPUtil.BufferedInputStreamExt. - * - * This is a hacky solution. - * - * @param inputStream input stream - * @return BufferedInputStreamExt - */ - public static InputStream getDecoderStream(InputStream inputStream) throws IOException { - BufferedInputStream buf = new BufferedInputStream(inputStream, 512); - InputStream decoderStream = PGPUtilWrapper.getDecoderStream(buf); - // Data is not armored -> return - if (decoderStream instanceof BufferedInputStream) { - return decoderStream; - } - // Wrap armored input stream with fix for #159 - decoderStream = CRCingArmoredInputStreamWrapper.possiblyWrap(decoderStream); - - decoderStream = PGPUtil.getDecoderStream(decoderStream); - return decoderStream; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/ArmoredInputStreamFactory.java b/pgpainless-core/src/main/java/org/pgpainless/util/ArmoredInputStreamFactory.java deleted file mode 100644 index 0c5ceeaf..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/util/ArmoredInputStreamFactory.java +++ /dev/null @@ -1,29 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.util; - -import java.io.IOException; -import java.io.InputStream; - -import org.bouncycastle.bcpg.ArmoredInputStream; - -public final class ArmoredInputStreamFactory { - - private ArmoredInputStreamFactory() { - - } - - public static ArmoredInputStream get(InputStream inputStream) throws IOException { - if (inputStream instanceof CRCingArmoredInputStreamWrapper) { - return (ArmoredInputStream) inputStream; - } - if (inputStream instanceof ArmoredInputStream) { - return new CRCingArmoredInputStreamWrapper((ArmoredInputStream) inputStream); - } - - ArmoredInputStream armorIn = new ArmoredInputStream(inputStream); - return new CRCingArmoredInputStreamWrapper(armorIn); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/ArmoredOutputStreamFactory.java b/pgpainless-core/src/main/java/org/pgpainless/util/ArmoredOutputStreamFactory.java deleted file mode 100644 index 2ccf7536..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/util/ArmoredOutputStreamFactory.java +++ /dev/null @@ -1,85 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.util; - -import java.io.OutputStream; - -import org.bouncycastle.bcpg.ArmoredOutputStream; - -/** - * Factory to create configured {@link ArmoredOutputStream ArmoredOutputStreams}. - * The configuration entails setting custom version and comment headers. - */ -public final class ArmoredOutputStreamFactory { - - public static final String PGPAINLESS = "PGPainless"; - private static String version = PGPAINLESS; - private static String[] comment = new String[0]; - - private ArmoredOutputStreamFactory() { - - } - - /** - * Wrap an {@link OutputStream} inside a preconfigured {@link ArmoredOutputStream}. - * - * @param outputStream inner stream - * @return armored output stream - */ - public static ArmoredOutputStream get(OutputStream outputStream) { - ArmoredOutputStream armoredOutputStream = new ArmoredOutputStream(outputStream); - armoredOutputStream.setHeader(ArmorUtils.HEADER_VERSION, version); - for (String comment : comment) { - ArmorUtils.addCommentHeader(armoredOutputStream, comment); - } - return armoredOutputStream; - } - - /** - * Overwrite the version header of ASCII armors with a custom value. - * Newlines in the version info string result in multiple version header entries. - * - * @param versionString version string - */ - public static void setVersionInfo(String versionString) { - if (versionString == null || versionString.trim().isEmpty()) { - throw new IllegalArgumentException("Version Info MUST NOT be null NOR empty."); - } - version = versionString; - } - - /** - * Reset the version header to its default value of {@link #PGPAINLESS}. - */ - public static void resetVersionInfo() { - version = PGPAINLESS; - } - - /** - * Set a comment header value in the ASCII armor header. - * If the comment contains newlines, it will be split into multiple header entries. - * - * @param commentString comment - */ - public static void setComment(String commentString) { - if (commentString == null) { - throw new IllegalArgumentException("Comment cannot be null."); - } - String trimmed = commentString.trim(); - if (trimmed.isEmpty()) { - throw new IllegalArgumentException("Comment cannot be empty."); - } - - String[] lines = commentString.split("\n"); - comment = lines; - } - - /** - * Reset to the default of no comment headers. - */ - public static void resetComment() { - comment = new String[0]; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/BCUtil.java b/pgpainless-core/src/main/java/org/pgpainless/util/BCUtil.java deleted file mode 100644 index f548af98..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/util/BCUtil.java +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.util; - -import java.security.NoSuchAlgorithmException; - -import org.bouncycastle.asn1.ASN1ObjectIdentifier; -import org.bouncycastle.bcpg.ECPublicBCPGKey; -import org.bouncycastle.openpgp.PGPPublicKey; - -public final class BCUtil { - - private BCUtil() { - - } - - /** - * Utility method to get the bit strength of OpenPGP keys. - * Bouncycastle is lacking support for some keys (eg. EdDSA, X25519), so this method - * manually derives the bit strength from the keys curves OID. - * - * @param key key - * @return bit strength - */ - public static int getBitStrength(PGPPublicKey key) throws NoSuchAlgorithmException { - int bitStrength = key.getBitStrength(); - - if (bitStrength == -1) { - // BC's PGPPublicKey.getBitStrength() does fail for some keys (EdDSA, X25519) - // therefore we manually set the bit strength. - // see https://github.com/bcgit/bc-java/issues/972 - - ASN1ObjectIdentifier oid = ((ECPublicBCPGKey) key.getPublicKeyPacket().getKey()).getCurveOID(); - if (oid.getId().equals("1.3.6.1.4.1.11591.15.1")) { - // ed25519 is 256 bits - bitStrength = 256; - } else if (oid.getId().equals("1.3.6.1.4.1.3029.1.5.1")) { - // curvey25519 is 256 bits - bitStrength = 256; - } else { - throw new NoSuchAlgorithmException("Unknown curve: " + oid.getId()); - } - - } - return bitStrength; - } - -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/CRCingArmoredInputStreamWrapper.java b/pgpainless-core/src/main/java/org/pgpainless/util/CRCingArmoredInputStreamWrapper.java deleted file mode 100644 index 3fa84d53..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/util/CRCingArmoredInputStreamWrapper.java +++ /dev/null @@ -1,163 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.util; - -import java.io.IOException; -import java.io.InputStream; - -import org.bouncycastle.bcpg.ArmoredInputStream; - -/** - * Utility class that causes read(bytes, offset, length) to properly throw exceptions - * caused by faulty CRC checksums. - * - * Furthermore, this class swallows exceptions from BC's ArmoredInputStream that are caused - * by missing CRC checksums. - */ -public class CRCingArmoredInputStreamWrapper extends ArmoredInputStream { - - private final ArmoredInputStream inputStream; - - public CRCingArmoredInputStreamWrapper(ArmoredInputStream inputStream) throws IOException { - super(inputStream, false); - this.inputStream = inputStream; - } - - public static InputStream possiblyWrap(InputStream inputStream) throws IOException { - if (inputStream instanceof CRCingArmoredInputStreamWrapper) { - return inputStream; - } - - if (inputStream instanceof ArmoredInputStream) { - return new CRCingArmoredInputStreamWrapper((ArmoredInputStream) inputStream); - } - - return inputStream; - } - - @Override - public boolean isClearText() { - return inputStream.isClearText(); - } - - @Override - public boolean isEndOfStream() { - return inputStream.isEndOfStream(); - } - - @Override - public String getArmorHeaderLine() { - return inputStream.getArmorHeaderLine(); - } - - @Override - public String[] getArmorHeaders() { - return inputStream.getArmorHeaders(); - } - - @Override - public int read() throws IOException { - try { - return inputStream.read(); - } catch (IOException e) { - if (e.getMessage().equals("no crc found in armored message.") || e.getMessage().equals("crc check not found.")) { - // swallow exception - return -1; - } else { - throw e; - } - } - } - - @Override - public int read(byte[] b) throws IOException { - return read(b, 0, b.length); - } - /** - * Reads up to len bytes of data from the input stream into - * an array of bytes. An attempt is made to read as many as - * len bytes, but a smaller number may be read. - * The number of bytes actually read is returned as an integer. - * - * The first byte read is stored into element b[off], the - * next one into b[off+1], and so on. The number of bytes read - * is, at most, equal to len. - * - * NOTE: We need to override the custom behavior of Java's {@link InputStream#read(byte[], int, int)}, - * as the upstream method silently swallows {@link IOException IOExceptions}. - * This would cause CRC checksum errors to go unnoticed. - * - * @see Related BC bug report - * @param b byte array - * @param off offset at which we start writing data to the array - * @param len number of bytes we write into the array - * @return total number of bytes read into the buffer - * - * @throws IOException if an exception happens AT ANY POINT - */ - @Override - public int read(byte[] b, int off, int len) throws IOException { - checkIndexSize(b.length, off, len); - - if (len == 0) { - return 0; - } - - int c = read(); - if (c == -1) { - return -1; - } - b[off] = (byte) c; - - int i = 1; - for (; i < len ; i++) { - c = read(); - if (c == -1) { - break; - } - b[off + i] = (byte) c; - } - return i; - } - - private void checkIndexSize(int size, int off, int len) { - if (off < 0 || len < 0) { - throw new IndexOutOfBoundsException("Offset and length cannot be negative."); - } - if (size < off + len) { - throw new IndexOutOfBoundsException("Invalid offset and length."); - } - } - - @Override - public long skip(long n) throws IOException { - return inputStream.skip(n); - } - - @Override - public int available() throws IOException { - return inputStream.available(); - } - - @Override - public void close() throws IOException { - inputStream.close(); - } - - @Override - public synchronized void mark(int readlimit) { - inputStream.mark(readlimit); - } - - @Override - public synchronized void reset() throws IOException { - inputStream.reset(); - } - - @Override - public boolean markSupported() { - return inputStream.markSupported(); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/CollectionUtils.java b/pgpainless-core/src/main/java/org/pgpainless/util/CollectionUtils.java deleted file mode 100644 index dcae3240..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/util/CollectionUtils.java +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.util; - -import java.lang.reflect.Array; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; - -public final class CollectionUtils { - - private CollectionUtils() { - - } - - public static List iteratorToList(Iterator iterator) { - List items = new ArrayList<>(); - while (iterator.hasNext()) { - I item = iterator.next(); - items.add(item); - } - return items; - } - - public static T[] concat(T t, T[] ts) { - T[] concat = (T[]) Array.newInstance(t.getClass(), ts.length + 1); - concat[0] = t; - System.arraycopy(ts, 0, concat, 1, ts.length); - return concat; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/DateUtil.java b/pgpainless-core/src/main/java/org/pgpainless/util/DateUtil.java deleted file mode 100644 index 3f00ddb3..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/util/DateUtil.java +++ /dev/null @@ -1,56 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.util; - -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.TimeZone; - -public final class DateUtil { - - private DateUtil() { - - } - - public static SimpleDateFormat getParser() { - SimpleDateFormat parser = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z"); - parser.setTimeZone(TimeZone.getTimeZone("UTC")); - return parser; - } - - /** - * Parse a UTC timestamp into a date. - * - * @param dateString timestamp - * @return date - */ - public static Date parseUTCDate(String dateString) { - try { - return getParser().parse(dateString); - } catch (ParseException e) { - return null; - } - } - - /** - * Format a date as UTC timestamp. - * - * @param date date - * @return timestamp - */ - public static String formatUTCDate(Date date) { - return getParser().format(date); - } - - /** - * Return the current date "rounded" to UTC precision. - * - * @return now - */ - public static Date now() { - return parseUTCDate(formatUTCDate(new Date())); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/MultiMap.java b/pgpainless-core/src/main/java/org/pgpainless/util/MultiMap.java deleted file mode 100644 index ab3c7e67..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/util/MultiMap.java +++ /dev/null @@ -1,124 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.util; - -import javax.annotation.Nonnull; -import java.util.Collection; -import java.util.HashMap; -import java.util.LinkedHashSet; -import java.util.Map; -import java.util.Set; - -public class MultiMap { - - private final Map> map; - - public MultiMap() { - map = new HashMap<>(); - } - - public MultiMap(@Nonnull MultiMap other) { - this.map = new HashMap<>(); - for (K k : other.map.keySet()) { - map.put(k, new LinkedHashSet<>(other.map.get(k))); - } - } - - public MultiMap(@Nonnull Map> content) { - this.map = new HashMap<>(content); - } - - public int size() { - return map.size(); - } - - public boolean isEmpty() { - return map.isEmpty(); - } - - public boolean containsKey(K o) { - return map.containsKey(o); - } - - public boolean containsValue(V o) { - for (Set values : map.values()) { - if (values.contains(o)) return true; - } - return false; - } - - public Set get(K o) { - return map.get(o); - } - - public void put(K k, V v) { - Set values = map.get(k); - if (values == null) { - values = new LinkedHashSet<>(); - map.put(k, values); - } - values.add(v); - } - - public void put(K k, Set vs) { - for (V v : vs) { - put(k, v); - } - } - - public void removeAll(K o) { - map.remove(o); - } - - public void remove(K o, V v) { - Set vs = map.get(o); - if (vs == null) return; - vs.remove(v); - } - - public void putAll(MultiMap other) { - for (K key : other.keySet()) { - put(key, other.get(key)); - } - } - - public void clear() { - map.clear(); - } - - public Set keySet() { - return map.keySet(); - } - - public Collection> values() { - return map.values(); - } - - public Set>> entrySet() { - return map.entrySet(); - } - - @Override - public boolean equals(Object o) { - if (o == null) { - return false; - } - - if (!(o instanceof MultiMap)) { - return false; - } - - if (this == o) { - return true; - } - - return map.equals(((MultiMap) o).map); - } - - @Override - public int hashCode() { - return map.hashCode(); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/NotationRegistry.java b/pgpainless-core/src/main/java/org/pgpainless/util/NotationRegistry.java deleted file mode 100644 index 11aa7651..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/util/NotationRegistry.java +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.util; - -import java.util.HashSet; -import java.util.Set; - -/** - * Registry for known notations. - * Since signature verification must reject signatures with critical notations that are not known to the application, - * there must be some way to tell PGPainless which notations actually are known. - * - * To add a notation name, call {@link #addKnownNotation(String)}. - */ -public class NotationRegistry { - - private final Set knownNotations = new HashSet<>(); - - /** - * Add a known notation name into the registry. - * This will cause critical notations with that name to no longer invalidate the signature. - * - * @param notationName name of the notation - */ - public void addKnownNotation(String notationName) { - if (notationName == null) { - throw new NullPointerException("Notation name MUST NOT be null."); - } - knownNotations.add(notationName); - } - - /** - * Return true if the notation name is registered in the registry. - * - * @param notationName name of the notation - * @return true if notation is known, false otherwise. - */ - public boolean isKnownNotation(String notationName) { - return knownNotations.contains(notationName); - } - - /** - * Clear all known notations from the registry. - */ - public void clear() { - knownNotations.clear(); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/PGPUtilWrapper.java b/pgpainless-core/src/main/java/org/pgpainless/util/PGPUtilWrapper.java deleted file mode 100644 index c01dd03d..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/util/PGPUtilWrapper.java +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.util; - -import java.io.BufferedInputStream; -import java.io.IOException; -import java.io.InputStream; - -import org.bouncycastle.openpgp.PGPUtil; - -public final class PGPUtilWrapper { - - private PGPUtilWrapper() { - - } - - /** - * {@link PGPUtil#getDecoderStream(InputStream)} sometimes mistakens non-base64 data for base64 encoded data. - * - * This method expects a {@link BufferedInputStream} which is being reset in case an {@link IOException} is encountered. - * Therefore, we can properly handle non-base64 encoded data. - * - * @param buf buffered input stream - * @return input stream - * @throws IOException in case of an io error which is unrelated to base64 encoding - */ - public static InputStream getDecoderStream(BufferedInputStream buf) throws IOException { - buf.mark(512); - try { - return PGPUtil.getDecoderStream(buf); - } catch (IOException e) { - if (e.getMessage().contains("invalid characters encountered at end of base64 data")) { - buf.reset(); - return buf; - } - throw e; - } - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/Passphrase.java b/pgpainless-core/src/main/java/org/pgpainless/util/Passphrase.java deleted file mode 100644 index 1d33ea5b..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/util/Passphrase.java +++ /dev/null @@ -1,167 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.util; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.util.Arrays; - -public class Passphrase { - - private final Object lock = new Object(); - - private final char[] chars; - private boolean valid = true; - - /** - * Passphrase for keys etc. - * - * @param chars may be null for empty passwords. - */ - public Passphrase(@Nullable char[] chars) { - if (chars == null) { - this.chars = null; - } else { - char[] trimmed = removeTrailingAndLeadingWhitespace(chars); - if (trimmed.length == 0) { - this.chars = null; - } else { - this.chars = trimmed; - } - } - } - - /** - * Return a copy of the passed in char array, with leading and trailing whitespace characters removed. - * - * @param chars char array - * @return copy of char array with leading and trailing whitespace characters removed - */ - private static char[] removeTrailingAndLeadingWhitespace(char[] chars) { - int i = 0; - while (i < chars.length && isWhitespace(chars[i])) { - i++; - } - int j = chars.length - 1; - while (j >= i && isWhitespace(chars[j])) { - j--; - } - - char[] trimmed = new char[chars.length - i - (chars.length - 1 - j)]; - System.arraycopy(chars, i, trimmed, 0, trimmed.length); - - return trimmed; - } - - /** - * Return true, if the passed in char is a whitespace symbol (space, newline, tab). - * - * @param xar char - * @return true if whitespace - */ - private static boolean isWhitespace(char xar) { - return xar == ' ' || xar == '\n' || xar == '\t'; - } - - /** - * Create a {@link Passphrase} from a {@link String}. - * - * @param password password - * @return passphrase - */ - public static Passphrase fromPassword(@Nonnull String password) { - return new Passphrase(password.toCharArray()); - } - - /** - * Overwrite the char array with spaces and mark the {@link Passphrase} as invalidated. - */ - public void clear() { - synchronized (lock) { - if (chars != null) { - Arrays.fill(chars, ' '); - } - valid = false; - } - } - - /** - * Return a copy of the underlying char array. - * A return value of {@code null} represents no password. - * - * @return passphrase chars. - * - * @throws IllegalStateException in case the password has been cleared at this point. - */ - public @Nullable char[] getChars() { - synchronized (lock) { - if (!valid) { - throw new IllegalStateException("Passphrase has been cleared."); - } - - if (chars == null) { - return null; - } - - char[] copy = new char[chars.length]; - System.arraycopy(chars, 0, copy, 0, chars.length); - return copy; - } - } - - /** - * Return true if the passphrase has not yet been cleared. - * - * @return valid - */ - public boolean isValid() { - synchronized (lock) { - return valid; - } - } - - /** - * Return true if the passphrase represents no password. - * - * @return empty - */ - public boolean isEmpty() { - synchronized (lock) { - return valid && chars == null; - } - } - - /** - * Represents a {@link Passphrase} instance that represents no password. - * - * @return empty passphrase - */ - public static Passphrase emptyPassphrase() { - return new Passphrase(null); - } - - @Override - public int hashCode() { - if (getChars() == null) { - return 0; - } - return new String(getChars()).hashCode(); - } - - @Override - public boolean equals(Object obj) { - if (obj == null) { - return false; - } - if (this == obj) { - return true; - } - if (!(obj instanceof Passphrase)) { - return false; - } - Passphrase other = (Passphrase) obj; - return Arrays.equals(getChars(), other.getChars()); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/KeyRingSelectionStrategy.java b/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/KeyRingSelectionStrategy.java index a9f842e3..26180214 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/KeyRingSelectionStrategy.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/KeyRingSelectionStrategy.java @@ -8,11 +8,42 @@ import java.util.Set; import org.pgpainless.util.MultiMap; +/** + * Filter for selecting public / secret key rings based on identifiers (e.g. user-ids). + * + * @param Type of {@link org.bouncycastle.openpgp.PGPKeyRing} ({@link org.bouncycastle.openpgp.PGPSecretKeyRing} + * or {@link org.bouncycastle.openpgp.PGPPublicKeyRing}). + * @param Type of key ring collection (e.g. {@link org.bouncycastle.openpgp.PGPSecretKeyRingCollection} + * or {@link org.bouncycastle.openpgp.PGPPublicKeyRingCollection}). + * @param Type of key identifier + */ public interface KeyRingSelectionStrategy { + /** + * Return true, if the filter accepts the given

keyRing
based on the given
identifier
. + * + * @param identifier identifier + * @param keyRing key ring + * @return acceptance + */ boolean accept(O identifier, R keyRing); + /** + * Iterate of the given
keyRingCollection
and return a {@link Set} of all acceptable + * keyRings in the collection, based on the given
identifier
. + * + * @param identifier identifier + * @param keyRingCollection collection + * @return set of acceptable key rings + */ Set selectKeyRingsFromCollection(O identifier, C keyRingCollection); + /** + * Iterate over all keyRings in the given {@link MultiMap} of keyRingCollections and return a new {@link MultiMap} + * which for every identifier (key of the map) contains all acceptable keyRings based on that identifier. + * + * @param keyRingCollections MultiMap of identifiers and keyRingCollections. + * @return MultiMap of identifiers and acceptable keyRings. + */ MultiMap selectKeyRingsFromCollections(MultiMap keyRingCollections); } diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/PublicKeyRingSelectionStrategy.java b/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/PublicKeyRingSelectionStrategy.java index 038549bf..7dbf7c93 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/PublicKeyRingSelectionStrategy.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/PublicKeyRingSelectionStrategy.java @@ -7,12 +7,18 @@ package org.pgpainless.util.selection.keyring; import javax.annotation.Nonnull; import java.util.HashSet; import java.util.Iterator; +import java.util.Map; import java.util.Set; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; import org.pgpainless.util.MultiMap; +/** + * Abstract {@link KeyRingSelectionStrategy} for {@link PGPPublicKeyRing PGPPublicKeyRings}. + * + * @param Type of identifier + */ public abstract class PublicKeyRingSelectionStrategy implements KeyRingSelectionStrategy { @Override @@ -28,9 +34,9 @@ public abstract class PublicKeyRingSelectionStrategy implements KeyRingSelect @Override public MultiMap selectKeyRingsFromCollections(@Nonnull MultiMap keyRingCollections) { MultiMap keyRings = new MultiMap<>(); - for (O identifier : keyRingCollections.keySet()) { - for (PGPPublicKeyRingCollection collection : keyRingCollections.get(identifier)) { - keyRings.put(identifier, selectKeyRingsFromCollection(identifier, collection)); + for (Map.Entry> entry : keyRingCollections.entrySet()) { + for (PGPPublicKeyRingCollection collection : entry.getValue()) { + keyRings.plus(entry.getKey(), selectKeyRingsFromCollection(entry.getKey(), collection)); } } return keyRings; diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/SecretKeyRingSelectionStrategy.java b/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/SecretKeyRingSelectionStrategy.java index c54f81a5..9e57b575 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/SecretKeyRingSelectionStrategy.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/SecretKeyRingSelectionStrategy.java @@ -4,15 +4,21 @@ package org.pgpainless.util.selection.keyring; -import javax.annotation.Nonnull; import java.util.HashSet; import java.util.Iterator; +import java.util.Map; import java.util.Set; +import javax.annotation.Nonnull; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; import org.pgpainless.util.MultiMap; +/** + * Abstract {@link KeyRingSelectionStrategy} for {@link PGPSecretKeyRing PGPSecretKeyRings}. + * + * @param Type of identifier + */ public abstract class SecretKeyRingSelectionStrategy implements KeyRingSelectionStrategy { @Override public Set selectKeyRingsFromCollection(O identifier, @Nonnull PGPSecretKeyRingCollection keyRingCollection) { @@ -27,9 +33,9 @@ public abstract class SecretKeyRingSelectionStrategy implements KeyRingSelect @Override public MultiMap selectKeyRingsFromCollections(@Nonnull MultiMap keyRingCollections) { MultiMap keyRings = new MultiMap<>(); - for (O identifier : keyRingCollections.keySet()) { - for (PGPSecretKeyRingCollection collection : keyRingCollections.get(identifier)) { - keyRings.put(identifier, selectKeyRingsFromCollection(identifier, collection)); + for (Map.Entry> entry : keyRingCollections.entrySet()) { + for (PGPSecretKeyRingCollection collection : entry.getValue()) { + keyRings.plus(entry.getKey(), selectKeyRingsFromCollection(entry.getKey(), collection)); } } return keyRings; diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/impl/ExactUserId.java b/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/impl/ExactUserId.java index 5a05dc56..ffda8867 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/impl/ExactUserId.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/impl/ExactUserId.java @@ -4,38 +4,51 @@ package org.pgpainless.util.selection.keyring.impl; -import java.util.Iterator; +import java.util.List; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.pgpainless.key.util.KeyRingUtils; import org.pgpainless.util.selection.keyring.PublicKeyRingSelectionStrategy; import org.pgpainless.util.selection.keyring.SecretKeyRingSelectionStrategy; +/** + * Implementations of {@link org.pgpainless.util.selection.keyring.KeyRingSelectionStrategy} which select key rings + * based on the exact user-id. + */ public final class ExactUserId { private ExactUserId() { } + /** + * {@link PublicKeyRingSelectionStrategy} which accepts {@link PGPPublicKeyRing PGPPublicKeyRings} if those + * have a user-id which exactly matches the given
identifier
. + */ public static class PubRingSelectionStrategy extends PublicKeyRingSelectionStrategy { @Override public boolean accept(String identifier, PGPPublicKeyRing keyRing) { - Iterator userIds = keyRing.getPublicKey().getUserIDs(); - while (userIds.hasNext()) { - if (userIds.next().equals(identifier)) return true; + List userIds = KeyRingUtils.getUserIdsIgnoringInvalidUTF8(keyRing.getPublicKey()); + for (String userId : userIds) { + if (userId.equals(identifier)) return true; } return false; } } + /** + * {@link SecretKeyRingSelectionStrategy} which accepts {@link PGPSecretKeyRing PGPSecretKeyRings} if those + * have a user-id which exactly matches the given
identifier
. + */ public static class SecRingSelectionStrategy extends SecretKeyRingSelectionStrategy { @Override public boolean accept(String identifier, PGPSecretKeyRing keyRing) { - Iterator userIds = keyRing.getPublicKey().getUserIDs(); - while (userIds.hasNext()) { - if (userIds.next().equals(identifier)) return true; + List userIds = KeyRingUtils.getUserIdsIgnoringInvalidUTF8(keyRing.getPublicKey()); + for (String userId : userIds) { + if (userId.equals(identifier)) return true; } return false; } diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/impl/Whitelist.java b/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/impl/Whitelist.java index f296d8f1..934f5577 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/impl/Whitelist.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/impl/Whitelist.java @@ -13,12 +13,25 @@ import org.pgpainless.util.selection.keyring.PublicKeyRingSelectionStrategy; import org.pgpainless.util.selection.keyring.SecretKeyRingSelectionStrategy; import org.pgpainless.util.MultiMap; +/** + * Implementations of {@link org.pgpainless.util.selection.keyring.KeyRingSelectionStrategy} which accept PGP KeyRings + * based on a whitelist of acceptable keyIds. + */ public final class Whitelist { private Whitelist() { } + /** + * {@link org.pgpainless.util.selection.keyring.KeyRingSelectionStrategy} which accepts + * {@link PGPPublicKeyRing PGPPublicKeyRings} if the
whitelist
contains their primary key id. + * + * If the whitelist contains 123L for "alice@pgpainless.org", the key with primary key id 123L is + * acceptable for "alice@pgpainless.org". + * + * @param Type of identifier for {@link org.bouncycastle.openpgp.PGPPublicKeyRingCollection PGPPublicKeyRingCollections}. + */ public static class PubRingSelectionStrategy extends PublicKeyRingSelectionStrategy { private final MultiMap whitelist; @@ -43,6 +56,15 @@ public final class Whitelist { } } + /** + * {@link org.pgpainless.util.selection.keyring.KeyRingSelectionStrategy} which accepts + * {@link PGPSecretKeyRing PGPSecretKeyRings} if the
whitelist
contains their primary key id. + * + * If the whitelist contains 123L for "alice@pgpainless.org", the key with primary key id 123L is + * acceptable for "alice@pgpainless.org". + * + * @param Type of identifier for {@link org.bouncycastle.openpgp.PGPSecretKeyRingCollection PGPSecretKeyRingCollections}. + */ public static class SecRingSelectionStrategy extends SecretKeyRingSelectionStrategy { private final MultiMap whitelist; diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/impl/Wildcard.java b/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/impl/Wildcard.java index f7bab777..d1929028 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/impl/Wildcard.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/impl/Wildcard.java @@ -9,6 +9,9 @@ import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.pgpainless.util.selection.keyring.PublicKeyRingSelectionStrategy; import org.pgpainless.util.selection.keyring.SecretKeyRingSelectionStrategy; +/** + * Implementations of {@link org.pgpainless.util.selection.keyring.KeyRingSelectionStrategy} which accept all keyRings. + */ public final class Wildcard { private Wildcard() { diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/impl/XMPP.java b/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/impl/XMPP.java index 11c3c8ff..edc38006 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/impl/XMPP.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/impl/XMPP.java @@ -7,12 +7,22 @@ package org.pgpainless.util.selection.keyring.impl; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing; +/** + * Implementations of {@link org.pgpainless.util.selection.keyring.KeyRingSelectionStrategy} which accept KeyRings + * containing a given XMPP address of the format "xmpp:alice@pgpainless.org". + */ public final class XMPP { private XMPP() { } + /** + * {@link org.pgpainless.util.selection.keyring.PublicKeyRingSelectionStrategy} which accepts a given + * {@link PGPPublicKeyRing} if its primary key has a user-id that matches the given
jid
. + * + * The argument
jid
can either contain the prefix "xmpp:", or not, the result will be the same. + */ public static class PubRingSelectionStrategy extends ExactUserId.PubRingSelectionStrategy { @Override @@ -24,6 +34,12 @@ public final class XMPP { } } + /** + * {@link org.pgpainless.util.selection.keyring.SecretKeyRingSelectionStrategy} which accepts a given + * {@link PGPSecretKeyRing} if its primary key has a user-id that matches the given
jid
. + * + * The argument
jid
can either contain the prefix "xmpp:", or not, the result will be the same. + */ public static class SecRingSelectionStrategy extends ExactUserId.SecRingSelectionStrategy { @Override diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/selection/userid/SelectUserId.java b/pgpainless-core/src/main/java/org/pgpainless/util/selection/userid/SelectUserId.java deleted file mode 100644 index 3f5cc98b..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/util/selection/userid/SelectUserId.java +++ /dev/null @@ -1,119 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.util.selection.userid; - -import java.util.ArrayList; -import java.util.List; - -import org.bouncycastle.openpgp.PGPKeyRing; -import org.pgpainless.PGPainless; - -public abstract class SelectUserId { - - protected abstract boolean accept(String userId); - - public List selectUserIds(PGPKeyRing keyRing) { - List userIds = PGPainless.inspectKeyRing(keyRing).getValidUserIds(); - return selectUserIds(userIds); - } - - public List selectUserIds(List userIds) { - List selected = new ArrayList<>(); - for (String userId : userIds) { - if (accept(userId)) { - selected.add(userId); - } - } - return selected; - } - - public String firstMatch(PGPKeyRing keyRing) { - return firstMatch(selectUserIds(keyRing)); - } - - public String firstMatch(List userIds) { - for (String userId : userIds) { - if (accept(userId)) { - return userId; - } - } - return null; - } - - public static SelectUserId containsSubstring(String query) { - return new SelectUserId() { - @Override - protected boolean accept(String userId) { - return userId.contains(query); - } - }; - } - - public static SelectUserId exactMatch(String query) { - return new SelectUserId() { - @Override - protected boolean accept(String userId) { - return userId.equals(query); - } - }; - } - - public static SelectUserId startsWith(String substring) { - return new SelectUserId() { - @Override - protected boolean accept(String userId) { - return userId.startsWith(substring); - } - }; - } - - public static SelectUserId containsEmailAddress(String email) { - return containsSubstring(email.matches("^<.+>$") ? email : '<' + email + '>'); - } - - public static SelectUserId validUserId(PGPKeyRing keyRing) { - return new SelectUserId() { - @Override - protected boolean accept(String userId) { - return PGPainless.inspectKeyRing(keyRing).isUserIdValid(userId); - } - }; - } - - public static SelectUserId and(SelectUserId... strategies) { - return new SelectUserId() { - @Override - protected boolean accept(String userId) { - boolean accept = true; - for (SelectUserId strategy : strategies) { - accept &= strategy.accept(userId); - } - return accept; - } - }; - } - - public static SelectUserId or(SelectUserId... strategies) { - return new SelectUserId() { - @Override - protected boolean accept(String userId) { - boolean accept = false; - for (SelectUserId strategy : strategies) { - accept |= strategy.accept(userId); - } - return accept; - } - }; - } - - public static SelectUserId not(SelectUserId strategy) { - return new SelectUserId() { - @Override - protected boolean accept(String userId) { - return !strategy.accept(userId); - } - }; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/selection/userid/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/util/selection/userid/package-info.java deleted file mode 100644 index 5b70f412..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/util/selection/userid/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * UserID selection strategies. - */ -package org.pgpainless.util.selection.userid; diff --git a/pgpainless-core/src/main/kotlin/openpgp/DateExtensions.kt b/pgpainless-core/src/main/kotlin/openpgp/DateExtensions.kt new file mode 100644 index 00000000..2763cb55 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/openpgp/DateExtensions.kt @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package openpgp + +import java.text.ParseException +import java.text.SimpleDateFormat +import java.util.* + +/** + * Return a new date which represents this date plus the given amount of seconds added. + * + * Since '0' is a special date value in the OpenPGP specification (e.g. '0' means no expiration for + * expiration dates), this method will return 'null' if seconds is 0. + * + * @param date date + * @param seconds number of seconds to be added + * @return date plus seconds or null if seconds is '0' + */ +fun Date.plusSeconds(seconds: Long): Date? { + require(Long.MAX_VALUE - time > seconds) { + "Adding $seconds seconds to this date would cause time to overflow." + } + return if (seconds == 0L) null else Date(this.time + 1000 * seconds) +} + +val Date.asSeconds: Long + get() = time / 1000 + +fun Date.secondsTill(later: Date): Long { + require(this <= later) { "Timestamp MUST be before the later timestamp." } + return later.asSeconds - this.asSeconds +} + +/** Return a new [Date] instance with this instance's time floored down to seconds precision. */ +fun Date.toSecondsPrecision(): Date { + return Date(asSeconds * 1000) +} + +internal val parser: SimpleDateFormat + // Java's SimpleDateFormat is not thread-safe, therefore we return a new instance on every + // invocation. + get() = + SimpleDateFormat("yyyy-MM-dd HH:mm:ss z").apply { timeZone = TimeZone.getTimeZone("UTC") } + +/** + * Format a date as UTC timestamp. + * + * @return timestamp + */ +fun Date.formatUTC(): String = parser.format(this) + +/** + * Parse a UTC timestamp into a date. + * + * @return date + */ +fun String.parseUTC(): Date { + return try { + parser.parse(this) + } catch (e: ParseException) { + throw IllegalArgumentException("Malformed UTC timestamp: $this", e) + } +} diff --git a/pgpainless-core/src/main/kotlin/openpgp/LongExtensions.kt b/pgpainless-core/src/main/kotlin/openpgp/LongExtensions.kt new file mode 100644 index 00000000..c6c318b3 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/openpgp/LongExtensions.kt @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package openpgp + +/** Format this Long as an OpenPGP key-ID (16 digit uppercase hex number). */ +fun Long.openPgpKeyId(): String { + return String.format("%016X", this).uppercase() +} + +/** Parse a Long form a 16 digit hex encoded OpenPgp key-ID. */ +fun Long.Companion.fromOpenPgpKeyId(hexKeyId: String): Long { + require("^[0-9A-Fa-f]{16}$".toRegex().matches(hexKeyId)) { + "Provided long key-id does not match expected format. " + + "A long key-id consists of 16 hexadecimal characters." + } + // Calling toLong() only fails with a NumberFormatException. + // Therefore, we call toULong(16).toLong(), which seems to work. + return hexKeyId.toULong(16).toLong() +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/PGPainless.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/PGPainless.kt new file mode 100644 index 00000000..81d9e605 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/PGPainless.kt @@ -0,0 +1,171 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless + +import java.io.OutputStream +import java.util.* +import org.bouncycastle.openpgp.PGPKeyRing +import org.bouncycastle.openpgp.PGPPublicKeyRing +import org.bouncycastle.openpgp.PGPSecretKeyRing +import org.bouncycastle.openpgp.PGPSignature +import org.pgpainless.decryption_verification.DecryptionBuilder +import org.pgpainless.encryption_signing.EncryptionBuilder +import org.pgpainless.key.certification.CertifyCertificate +import org.pgpainless.key.generation.KeyRingBuilder +import org.pgpainless.key.generation.KeyRingTemplates +import org.pgpainless.key.info.KeyRingInfo +import org.pgpainless.key.modification.secretkeyring.SecretKeyRingEditor +import org.pgpainless.key.parsing.KeyRingReader +import org.pgpainless.key.util.KeyRingUtils +import org.pgpainless.policy.Policy +import org.pgpainless.util.ArmorUtils + +class PGPainless private constructor() { + + companion object { + + /** + * Generate a fresh OpenPGP key ring from predefined templates. + * + * @return templates + */ + @JvmStatic fun generateKeyRing() = KeyRingTemplates() + + /** + * Build a custom OpenPGP key ring. + * + * @return builder + */ + @JvmStatic fun buildKeyRing() = KeyRingBuilder() + + /** + * Read an existing OpenPGP key ring. + * + * @return builder + */ + @JvmStatic fun readKeyRing() = KeyRingReader() + + /** + * Extract a public key certificate from a secret key. + * + * @param secretKey secret key + * @return public key certificate + */ + @JvmStatic + fun extractCertificate(secretKey: PGPSecretKeyRing) = + KeyRingUtils.publicKeyRingFrom(secretKey) + + /** + * Merge two copies of the same certificate (e.g. an old copy, and one retrieved from a key + * server) together. + * + * @param originalCopy local, older copy of the cert + * @param updatedCopy updated, newer copy of the cert + * @return merged certificate + * @throws PGPException in case of an error + */ + @JvmStatic + fun mergeCertificate(originalCopy: PGPPublicKeyRing, updatedCopy: PGPPublicKeyRing) = + PGPPublicKeyRing.join(originalCopy, updatedCopy) + + /** + * Wrap a key or certificate in ASCII armor. + * + * @param key key or certificate + * @return ascii armored string + * @throws IOException in case of an error during the armoring process + */ + @JvmStatic + fun asciiArmor(key: PGPKeyRing) = + if (key is PGPSecretKeyRing) ArmorUtils.toAsciiArmoredString(key) + else ArmorUtils.toAsciiArmoredString(key as PGPPublicKeyRing) + + /** + * Wrap a key of certificate in ASCII armor and write the result into the given + * [OutputStream]. + * + * @param key key or certificate + * @param outputStream output stream + * @throws IOException in case of an error during the armoring process + */ + @JvmStatic + fun asciiArmor(key: PGPKeyRing, outputStream: OutputStream) { + val armorOut = ArmorUtils.toAsciiArmoredStream(key, outputStream) + key.encode(armorOut) + armorOut.close() + } + + /** + * Wrap the detached signature in ASCII armor. + * + * @param signature detached signature + * @return ascii armored string + * @throws IOException in case of an error during the armoring process + */ + @JvmStatic + fun asciiArmor(signature: PGPSignature) = ArmorUtils.toAsciiArmoredString(signature) + + /** + * Create an [EncryptionBuilder], which can be used to encrypt and/or sign data using + * OpenPGP. + * + * @return builder + */ + @JvmStatic fun encryptAndOrSign() = EncryptionBuilder() + + /** + * Create a [DecryptionBuilder], which can be used to decrypt and/or verify data using + * OpenPGP. + * + * @return builder + */ + @JvmStatic fun decryptAndOrVerify() = DecryptionBuilder() + + /** + * Make changes to a secret key at the given reference time. This method can be used to + * change key expiration dates and passphrases, or add/revoke user-ids and subkeys. + * + *

+ * After making the desired changes in the builder, the modified key can be extracted using + * [org.pgpainless.key.modification.secretkeyring.SecretKeyRingEditorInterface.done]. + * + * @param secretKeys secret key ring + * @param referenceTime reference time used as signature creation date + * @return builder + */ + @JvmStatic + @JvmOverloads + fun modifyKeyRing(secretKey: PGPSecretKeyRing, referenceTime: Date = Date()) = + SecretKeyRingEditor(secretKey, referenceTime) + + /** + * Quickly access information about a [org.bouncycastle.openpgp.PGPPublicKeyRing] / + * [PGPSecretKeyRing]. This method can be used to determine expiration dates, key flags and + * other information about a key at a specific time. + * + * @param keyRing key ring + * @param referenceTime date of inspection + * @return access object + */ + @JvmStatic + @JvmOverloads + fun inspectKeyRing(key: PGPKeyRing, referenceTime: Date = Date()) = + KeyRingInfo(key, referenceTime) + + /** + * Access, and make changes to PGPainless policy on acceptable/default algorithms etc. + * + * @return policy + */ + @JvmStatic fun getPolicy() = Policy.getInstance() + + /** + * Create different kinds of signatures on other keys. + * + * @return builder + */ + @JvmStatic fun certify() = CertifyCertificate() + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/AEADAlgorithm.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/AEADAlgorithm.kt new file mode 100644 index 00000000..6a3a6214 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/AEADAlgorithm.kt @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.algorithm + +enum class AEADAlgorithm(val algorithmId: Int, val ivLength: Int, val tagLength: Int) { + + /** + * Encrypt-then-Authenticate-then-Translate mode. + * https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-eax-mode + */ + EAX(1, 16, 16), + + /** + * Offset-Codebook mode. OCB is mandatory to implement in crypto-refresh. Favored by GnuPG. Is + * not yet FIPS compliant, but supported by most implementations and therefore favorable. + * https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-ocb-mode + */ + OCB(2, 15, 16), + + /** + * Galois/Counter-Mode. GCM is controversial. Some say it is hard to get right. Some + * implementations like GnuPG therefore avoid it. May be necessary to achieve FIPS compliance. + * https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-gcm-mode + */ + GCM(3, 12, 16), + ; + + companion object { + @JvmStatic + fun fromId(id: Int): AEADAlgorithm? { + return values().firstOrNull { algorithm -> algorithm.algorithmId == id } + } + + @JvmStatic + fun requireFromId(id: Int): AEADAlgorithm { + return fromId(id) ?: throw NoSuchElementException("No AEADAlgorithm found for id $id") + } + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/AlgorithmSuite.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/AlgorithmSuite.kt new file mode 100644 index 00000000..867bf1b8 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/AlgorithmSuite.kt @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.algorithm + +class AlgorithmSuite( + symmetricKeyAlgorithms: List, + hashAlgorithms: List, + compressionAlgorithms: List +) { + + val symmetricKeyAlgorithms: Set = symmetricKeyAlgorithms.toSet() + val hashAlgorithms: Set = hashAlgorithms.toSet() + val compressionAlgorithms: Set = compressionAlgorithms.toSet() + + companion object { + + @JvmStatic + val defaultSymmetricKeyAlgorithms = + listOf( + SymmetricKeyAlgorithm.AES_256, + SymmetricKeyAlgorithm.AES_192, + SymmetricKeyAlgorithm.AES_128) + + @JvmStatic + val defaultHashAlgorithms = + listOf( + HashAlgorithm.SHA512, + HashAlgorithm.SHA384, + HashAlgorithm.SHA256, + HashAlgorithm.SHA224) + + @JvmStatic + val defaultCompressionAlgorithms = + listOf( + CompressionAlgorithm.ZLIB, + CompressionAlgorithm.BZIP2, + CompressionAlgorithm.ZIP, + CompressionAlgorithm.UNCOMPRESSED) + + @JvmStatic + val defaultAlgorithmSuite = + AlgorithmSuite( + defaultSymmetricKeyAlgorithms, defaultHashAlgorithms, defaultCompressionAlgorithms) + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/CertificationType.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/CertificationType.kt new file mode 100644 index 00000000..5617109c --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/CertificationType.kt @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.algorithm + +enum class CertificationType(val signatureType: SignatureType) { + /** + * The issuer of this certification does not make any particular assertion as to how well the + * certifier has checked that the owner of the key is in fact the person described by the User + * ID. + */ + GENERIC(SignatureType.GENERIC_CERTIFICATION), + + /** + * The issuer of this certification has not done any verification of the claim that the owner of + * this key is the User ID specified. + */ + NONE(SignatureType.NO_CERTIFICATION), + + /** + * The issuer of this certification has done some casual verification of the claim of identity. + */ + CASUAL(SignatureType.CASUAL_CERTIFICATION), + + /** + * The issuer of this certification has done some casual verification of the claim of identity. + */ + POSITIVE(SignatureType.POSITIVE_CERTIFICATION), + ; + + fun asSignatureType() = signatureType +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/CompressionAlgorithm.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/CompressionAlgorithm.kt new file mode 100644 index 00000000..da4085bd --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/CompressionAlgorithm.kt @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.algorithm + +/** + * Enumeration of possible compression algorithms. + * + * See also [RFC4880 - Compression Algorithm Tags](https://tools.ietf.org/html/rfc4880#section-9.3) + */ +enum class CompressionAlgorithm(val algorithmId: Int) { + + UNCOMPRESSED(0), + ZIP(1), + ZLIB(2), + BZIP2(3), + ; + + companion object { + + /** + * Return the [CompressionAlgorithm] value that corresponds to the provided numerical id. If + * an invalid id is provided, null is returned. + * + * @param id id + * @return compression algorithm + */ + @JvmStatic + fun fromId(id: Int): CompressionAlgorithm? { + return values().firstOrNull { c -> c.algorithmId == id } + } + + /** + * Return the [CompressionAlgorithm] value that corresponds to the provided numerical id. If + * an invalid id is provided, throw an [NoSuchElementException]. + * + * @param id id + * @return compression algorithm + * @throws NoSuchElementException in case of an unmapped id + */ + @JvmStatic + fun requireFromId(id: Int): CompressionAlgorithm { + return fromId(id) + ?: throw NoSuchElementException("No CompressionAlgorithm found for id $id") + } + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/DocumentSignatureType.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/DocumentSignatureType.kt new file mode 100644 index 00000000..e41a4605 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/DocumentSignatureType.kt @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.algorithm + +enum class DocumentSignatureType(val signatureType: SignatureType) { + + /** Signature is calculated over the unchanged binary data. */ + BINARY_DOCUMENT(SignatureType.BINARY_DOCUMENT), + + /** + * The signature is calculated over the text data with its line endings converted to ``. + */ + CANONICAL_TEXT_DOCUMENT(SignatureType.CANONICAL_TEXT_DOCUMENT), +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/EncryptionPurpose.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/EncryptionPurpose.kt new file mode 100644 index 00000000..1b4bbe6e --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/EncryptionPurpose.kt @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.algorithm + +enum class EncryptionPurpose { + /** The stream will encrypt communication that goes over the wire. E.g. EMail, Chat... */ + COMMUNICATIONS, + /** The stream will encrypt data at rest. E.g. Encrypted backup... */ + STORAGE, + /** The stream will use keys with either flags to encrypt the data. */ + ANY +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/Feature.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/Feature.kt new file mode 100644 index 00000000..ec1cfe21 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/Feature.kt @@ -0,0 +1,90 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.algorithm + +/** + * An enumeration of features that may be set in the feature subpacket. + * + * See [RFC4880: Features](https://tools.ietf.org/html/rfc4880#section-5.2.3.24) See + * [crypto-refresh: Features](https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-features) + * See + * [LibrePGP: Features](https://www.ietf.org/archive/id/draft-koch-librepgp-00.html#name-features) + */ +enum class Feature(val featureId: Byte) { + + /** + * Support for Symmetrically Encrypted Integrity Protected Data Packets (version 1) using + * Modification Detection Code Packets. + * + * See + * [RFC-4880 §5.14: Modification Detection Code Packet](https://tools.ietf.org/html/rfc4880#section-5.14) + * See + * [Crypto-Refresh: Features](https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-features) + */ + MODIFICATION_DETECTION(0x01), + + /** + * Support for OCB Encrypted Data (AEAD) as defined in LibrePGP (NON-STANDARD!) If a key + * announces this feature, it signals support for consuming OCB Encrypted Data Packets. + * + * NOTE: PGPAINLESS DOES NOT YET SUPPORT THIS FEATURE!!! NOTE: This value is currently RESERVED. + * + * See + * [LibrePGP: OCB Encrypted Data Packet](https://www.ietf.org/archive/id/draft-koch-librepgp-00.html#name-features) + */ + LIBREPGP_OCB_ENCRYPTED_DATA(0x02), + + /** + * If a key announces this feature, it is a version 5 public key as defined in LibrePGP + * (NON-STANDARD!). The version 5 format is similar to the version 4 format except for the + * addition of a count for the key material. This count helps to parse secret key packets (which + * are an extension of the public key packet format) in the case of an unknown algorithm. In + * addition, fingerprints of version 5 keys are calculated differently from version 4 keys. + * + * NOTE: PGPAINLESS DOES NOT YET SUPPORT THIS FEATURE!!! NOTE: This value is currently RESERVED. + * + * See + * [LibrePGP: Version 5 Public-Key Format](https://www.ietf.org/archive/id/draft-koch-librepgp-00.html#name-features) + */ + LIBREPGP_VERSION_5_PUBLIC_KEY(0x04), + + /** + * Support for Symmetrically Encrypted Integrity Protected Data packet version 2. This packet + * protects data using AEAD encryption as defined in crypto-refresh. + * + * See + * [crypto-refresh-06 §5.13.2. Version 2 Sym. Encrypted Integrity Protected Data Packet Format](https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-06.html#version-two-seipd) + * See + * [crypto-refresh: Features](https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-features) + */ + MODIFICATION_DETECTION_2(0x08), + ; + + companion object { + @JvmStatic + fun fromId(id: Byte): Feature? { + return values().firstOrNull { f -> f.featureId == id } + } + + @JvmStatic + fun requireFromId(id: Byte): Feature { + return fromId(id) ?: throw NoSuchElementException("Unknown feature id encountered: $id") + } + + @JvmStatic + fun fromBitmask(bitmask: Int): List { + return values().filter { it.featureId.toInt() and bitmask != 0 } + } + + @JvmStatic + fun toBitmask(vararg features: Feature): Byte { + return features + .map { it.featureId.toInt() } + .reduceOrNull { mask, f -> mask or f } + ?.toByte() + ?: 0 + } + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/HashAlgorithm.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/HashAlgorithm.kt new file mode 100644 index 00000000..3360e7fe --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/HashAlgorithm.kt @@ -0,0 +1,70 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.algorithm + +/** + * An enumeration of different hashing algorithms. + * + * See [RFC4880: Hash Algorithms](https://tools.ietf.org/html/rfc4880#section-9.4) + */ +enum class HashAlgorithm(val algorithmId: Int, val algorithmName: String) { + + @Deprecated("MD5 is deprecated") MD5(1, "MD5"), + SHA1(2, "SHA1"), + RIPEMD160(3, "RIPEMD160"), + SHA256(8, "SHA256"), + SHA384(9, "SHA384"), + SHA512(10, "SHA512"), + SHA224(11, "SHA224"), + SHA3_256(12, "SHA3-256"), + SHA3_512(14, "SHA3-512"), + ; + + companion object { + /** + * Return the [HashAlgorithm] value that corresponds to the provided algorithm id. If an + * invalid algorithm id was provided, null is returned. + * + * @param id numeric id + * @return enum value + */ + @JvmStatic + fun fromId(id: Int): HashAlgorithm? { + return values().firstOrNull { h -> h.algorithmId == id } + } + + /** + * Return the [HashAlgorithm] value that corresponds to the provided algorithm id. If an + * invalid algorithm id was provided, throw a [NoSuchElementException]. + * + * @param id algorithm id + * @return enum value + * @throws NoSuchElementException in case of an unknown algorithm id + */ + @JvmStatic + fun requireFromId(id: Int): HashAlgorithm { + return fromId(id) ?: throw NoSuchElementException("No HashAlgorithm found for id $id") + } + + /** + * Return the [HashAlgorithm] value that corresponds to the provided name. If an invalid + * algorithm name was provided, null is returned. + * + * See + * [RFC4880: §9.4 Hash Algorithms](https://datatracker.ietf.org/doc/html/rfc4880#section-9.4) + * for a list of algorithms and names. + * + * @param name text name + * @return enum value + */ + @JvmStatic + fun fromName(name: String): HashAlgorithm? { + return name.uppercase().let { algoName -> + values().firstOrNull { it.algorithmName == algoName } + ?: values().firstOrNull { it.algorithmName == algoName.replace("-", "") } + } + } + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/KeyFlag.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/KeyFlag.kt new file mode 100644 index 00000000..c5c4e103 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/KeyFlag.kt @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.algorithm + +enum class KeyFlag(val flag: Int) { + + /** This key may be used to certify third-party keys. */ + CERTIFY_OTHER(1), + + /** This key may be used to sign data. */ + SIGN_DATA(2), + + /** This key may be used to encrypt communications. */ + ENCRYPT_COMMS(4), + + /** This key may be used to encrypt storage. */ + ENCRYPT_STORAGE(8), + + /** The private component of this key may have been split by a secret-sharing mechanism. */ + SPLIT(16), + + /** This key may be used for authentication. */ + AUTHENTICATION(32), + + /** The private component of this key may be in the possession of more than one person. */ + SHARED(128), + ; + + companion object { + + /** + * Convert a bitmask into a list of [KeyFlags][KeyFlag]. + * + * @param bitmask bitmask + * @return list of key flags encoded by the bitmask + */ + @JvmStatic + fun fromBitmask(bitmask: Int): List { + return values().filter { it.flag and bitmask != 0 } + } + + /** + * Encode a list of [KeyFlags][KeyFlag] into a bitmask. + * + * @param flags list of flags + * @return bitmask + */ + @JvmStatic + fun toBitmask(vararg flags: KeyFlag): Int { + return flags.map { it.flag }.reduceOrNull { mask, f -> mask or f } ?: 0 + } + + /** + * Return true if the provided bitmask has the bit for the provided flag set. Return false + * if the mask does not contain the flag. + * + * @param mask bitmask + * @param flag flag to be tested for + * @return true if flag is set, false otherwise + */ + @JvmStatic + fun hasKeyFlag(mask: Int, flag: KeyFlag): Boolean { + return mask and flag.flag == flag.flag + } + + @JvmStatic + fun containsAny(mask: Int, vararg flags: KeyFlag): Boolean { + return flags.any { hasKeyFlag(mask, it) } + } + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/OpenPgpPacket.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/OpenPgpPacket.kt new file mode 100644 index 00000000..17ec90e4 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/OpenPgpPacket.kt @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.algorithm + +enum class OpenPgpPacket(val tag: Int) { + PKESK(1), + SIG(2), + SKESK(3), + OPS(4), + SK(5), + PK(6), + SSK(7), + COMP(8), + SED(9), + MARKER(10), + LIT(11), + TRUST(12), + UID(13), + PSK(14), + UATTR(17), + SEIPD(18), + MDC(19), + OED(20), + PADDING(21), + EXP_1(60), + EXP_2(61), + EXP_3(62), + EXP_4(63), + ; + + companion object { + @JvmStatic + fun fromTag(tag: Int): OpenPgpPacket? { + return values().firstOrNull { it.tag == tag } + } + + @JvmStatic + fun requireFromTag(tag: Int): OpenPgpPacket { + return fromTag(tag) + ?: throw NoSuchElementException("No OpenPGP packet known for tag $tag") + } + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/PublicKeyAlgorithm.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/PublicKeyAlgorithm.kt new file mode 100644 index 00000000..b8fc6836 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/PublicKeyAlgorithm.kt @@ -0,0 +1,94 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.algorithm + +/** + * Enumeration of public key algorithms as defined in RFC4880. + * + * See [RFC4880: Public-Key Algorithms](https://tools.ietf.org/html/rfc4880#section-9.1) + */ +enum class PublicKeyAlgorithm( + val algorithmId: Int, + val signingCapable: Boolean, + val encryptionCapable: Boolean +) { + + /** RSA capable of encryption and signatures. */ + RSA_GENERAL(1, true, true), + + /** + * RSA with usage encryption. + * + * @deprecated see Deprecation + * notice + */ + @Deprecated("RSA_ENCRYPT is deprecated in favor of RSA_GENERAL", ReplaceWith("RSA_GENERAL")) + RSA_ENCRYPT(2, false, true), + + /** + * RSA with usage of creating signatures. + * + * @deprecated see Deprecation + * notice + */ + @Deprecated("RSA_SIGN is deprecated in favor of RSA_GENERAL", ReplaceWith("RSA_GENERAL")) + RSA_SIGN(3, true, false), + + /** ElGamal with usage encryption. */ + ELGAMAL_ENCRYPT(16, false, true), + + /** Digital Signature Algorithm. */ + DSA(17, true, false), + + /** Elliptic Curve Diffie-Hellman. */ + ECDH(18, false, true), + + /** Elliptic Curve Digital Signature Algorithm. */ + ECDSA(19, true, false), + + /** + * ElGamal General. + * + * @deprecated see Deprecation + * notice + */ + @Deprecated("ElGamal is deprecated") ELGAMAL_GENERAL(20, true, true), + + /** Diffie-Hellman key exchange algorithm. */ + DIFFIE_HELLMAN(21, false, true), + + /** Digital Signature Algorithm based on twisted Edwards Curves. */ + EDDSA_LEGACY(22, true, false), + + /** X25519 encryption algorithm. */ + X25519(25, false, true), + + /** X448 encryption algorithm. */ + X448(26, false, true), + + /** Ed25519 signature algorithm. */ + ED25519(27, true, false), + + /** Ed448 signature algorithm. */ + ED448(28, true, false), + ; + + fun isSigningCapable(): Boolean = signingCapable + + fun isEncryptionCapable(): Boolean = encryptionCapable + + companion object { + @JvmStatic + fun fromId(id: Int): PublicKeyAlgorithm? { + return values().firstOrNull { it.algorithmId == id } + } + + @JvmStatic + fun requireFromId(id: Int): PublicKeyAlgorithm { + return fromId(id) + ?: throw NoSuchElementException("No PublicKeyAlgorithm found for id $id") + } + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/RevocationState.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/RevocationState.kt new file mode 100644 index 00000000..ba288469 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/RevocationState.kt @@ -0,0 +1,83 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.algorithm + +import java.lang.AssertionError +import java.util.* +import kotlin.NoSuchElementException +import org.pgpainless.util.DateUtil + +class RevocationState private constructor(val type: RevocationStateType, private val _date: Date?) : + Comparable { + + val date: Date + get() { + if (!isSoftRevocation()) { + throw NoSuchElementException( + "RevocationStateType is not equal to 'softRevoked'. Cannot extract date.") + } + return _date!! + } + + private constructor(type: RevocationStateType) : this(type, null) + + fun isSoftRevocation() = type == RevocationStateType.softRevoked + + fun isHardRevocation() = type == RevocationStateType.hardRevoked + + fun isNotRevoked() = type == RevocationStateType.notRevoked + + companion object { + @JvmStatic fun notRevoked() = RevocationState(RevocationStateType.notRevoked) + + @JvmStatic + fun softRevoked(date: Date) = RevocationState(RevocationStateType.softRevoked, date) + + @JvmStatic fun hardRevoked() = RevocationState(RevocationStateType.hardRevoked) + } + + override fun compareTo(other: RevocationState): Int { + return when (type) { + RevocationStateType.notRevoked -> if (other.isNotRevoked()) 0 else -1 + RevocationStateType.softRevoked -> + if (other.isNotRevoked()) 1 + // Compare soft dates in reverse + else if (other.isSoftRevocation()) other.date.compareTo(date) else -1 + RevocationStateType.hardRevoked -> if (other.isHardRevocation()) 0 else 1 + else -> throw AssertionError("Unknown type: $type") + } + } + + override fun toString(): String { + return buildString { + append(type) + if (isSoftRevocation()) append(" (${DateUtil.formatUTCDate(date)})") + } + } + + override fun hashCode(): Int { + return type.hashCode() * 31 + if (isSoftRevocation()) date.hashCode() else 0 + } + + override fun equals(other: Any?): Boolean { + if (other == null) { + return false + } + if (this === other) { + return true + } + if (other !is RevocationState) { + return false + } + if (type != other.type) { + return false + } + if (isSoftRevocation()) { + return DateUtil.toSecondsPrecision(date).time == + DateUtil.toSecondsPrecision(other.date).time + } + return true + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/RevocationStateType.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/RevocationStateType.kt new file mode 100644 index 00000000..1f95ad7b --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/RevocationStateType.kt @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.algorithm + +enum class RevocationStateType { + /** Certificate is not revoked. */ + notRevoked, + + /** Certificate is revoked with a soft revocation. */ + softRevoked, + + /** Certificate is revoked with a hard revocation. */ + hardRevoked +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/SignatureSubpacket.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/SignatureSubpacket.kt new file mode 100644 index 00000000..a0efd618 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/SignatureSubpacket.kt @@ -0,0 +1,361 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.algorithm + +import org.bouncycastle.bcpg.SignatureSubpacketTags.* + +/** + * Enumeration of possible subpackets that might be found in the hashed and unhashed area of an + * OpenPGP signature. + * + * See + * [RFC4880: Signature Subpacket Specification](https://tools.ietf.org/html/rfc4880#section-5.2.3.1) + */ +enum class SignatureSubpacket(val code: Int) { + /** + * The time the signature was made. MUST be present in the hashed area of the signature. + * + * See [Signature Creation Time](https://tools.ietf.org/html/rfc4880#section-5.2.3.4) + */ + signatureCreationTime(2), + + /** + * The validity period of the signature. This is the number of seconds after the signature + * creation time that the signature expires. If this is not present or has a value of zero, it + * never expires. + * + * See [Signature Expiration Time](https://tools.ietf.org/html/rfc4880#section-5.2.3.10) + */ + signatureExpirationTime(3), + + /** + * Denotes whether the signature is exportable for other users. + * + * See [Exportable Certification](https://tools.ietf.org/html/rfc4880#section-5.2.3.11) + */ + exportableCertification(4), + + /** + * Signer asserts that the key is not only valid but also trustworthy at the specified level. + * Level 0 has the same meaning as an ordinary validity signature. Level 1 means that the signed + * key is asserted to be a valid, trusted introducer, with the 2nd octet of the body specifying + * the degree of trust. Level 2 means that the signed key is asserted to be trusted to issue + * level 1 trust signatures, i.e., that it is a "meta introducer". Generally, a level n trust + * signature asserts that a key is trusted to issue level n-1 trust signatures. The trust amount + * is in a range from 0-255, interpreted such that values less than 120 indicate partial trust + * and values of 120 or greater indicate complete trust. Implementations SHOULD emit values of + * 60 for partial trust and 120 for complete trust. + * + * See [Trust Signature](https://tools.ietf.org/html/rfc4880#section-5.2.3.13) + */ + trustSignature(5), + + /** + * Used in conjunction with trust Signature packets (of level greater 0) to limit the scope of + * trust that is extended. Only signatures by the target key on User IDs that match the regular + * expression in the body of this packet have trust extended by the trust Signature subpacket. + * The regular expression uses the same syntax as the Henry Spencer's "almost public domain" + * regular expression [REGEX] package. A description of the syntax is found in Section 8 below. + * + * See [Regular Expression](https://tools.ietf.org/html/rfc4880#section-5.2.3.14) + */ + regularExpression(6), + + /** + * Signature's revocability status. The packet body contains a Boolean flag indicating whether + * the signature is revocable. Signatures that are not revocable have any later revocation + * signatures ignored. They represent a commitment by the signer that he cannot revoke his + * signature for the life of his key. If this packet is not present, the signature is revocable. + * + * See [Revocable](https://tools.ietf.org/html/rfc4880#section-5.2.3.12) + */ + revocable(7), + + /** + * The validity period of the key. This is the number of seconds after the key creation time + * that the key expires. If this is not present or has a value of zero, the key never expires. + * This is found only on a self-signature. + * + * See [Key Expiration Time](https://tools.ietf.org/html/rfc4880#section-5.2.3.6) + */ + keyExpirationTime(9), + + /** Placeholder for backwards compatibility. */ + placeholder(10), + + /** + * Symmetric algorithm numbers that indicate which algorithms the keyholder prefers to use. The + * subpackets body is an ordered list of octets with the most preferred listed first. It is + * assumed that only algorithms listed are supported by the recipient's software. This is only + * found on a self-signature. + * + * See [Preferred Symmetric Algorithms](https://tools.ietf.org/html/rfc4880#section-5.2.3.7) + */ + preferredSymmetricAlgorithms(11), + + /** + * Authorizes the specified key to issue revocation signatures for this key. Class octet must + * have bit 0x80 set. If the bit 0x40 is set, then this means that the revocation information is + * sensitive. Other bits are for future expansion to other kinds of authorizations. This is + * found on a self-signature. + * + * If the "sensitive" flag is set, the keyholder feels this subpacket contains private trust + * information that describes a real-world sensitive relationship. If this flag is set, + * implementations SHOULD NOT export this signature to other users except in cases where the + * data needs to be available: when the signature is being sent to the designated revoker, or + * when it is accompanied by a revocation signature from that revoker. Note that it may be + * appropriate to isolate this subpacket within a separate signature so that it is not combined + * with other subpackets that need to be exported. + * + * See [Revocation Key](https://tools.ietf.org/html/rfc4880#section-5.2.3.15) + */ + revocationKey(12), + + /** + * The OpenPGP Key ID of the key issuing the signature. + * + * See [Issuer Key ID](https://tools.ietf.org/html/rfc4880#section-5.2.3.5) + */ + issuerKeyId(16), + + /** + * This subpacket describes a "notation" on the signature that the issuer wishes to make. The + * notation has a name and a value, each of which are strings of octets. There may be more than + * one notation in a signature. Notations can be used for any extension the issuer of the + * signature cares to make. The "flags" field holds four octets of flags. + * + * See [Notation Data](https://tools.ietf.org/html/rfc4880#section-5.2.3.16) + */ + notationData(20), + + /** + * Message digest algorithm numbers that indicate which algorithms the keyholder prefers to + * receive. Like the preferred symmetric algorithms, the list is ordered. This is only found on + * a self-signature. + * + * See [Preferred Hash Algorithms](https://tools.ietf.org/html/rfc4880#section-5.2.3.8) + */ + preferredHashAlgorithms(21), + + /** + * Compression algorithm numbers that indicate which algorithms the keyholder prefers to use. + * Like the preferred symmetric algorithms, the list is ordered. If this subpacket is not + * included, ZIP is preferred. A zero denotes that uncompressed data is preferred; the + * keyholder's software might have no compression software in that implementation. This is only + * found on a self-signature. + * + * See [Preferred Compressio Algorithms](https://tools.ietf.org/html/rfc4880#section-5.2.3.9) + */ + preferredCompressionAlgorithms(22), + + /** + * This is a list of one-bit flags that indicate preferences that the keyholder has about how + * the key is handled on a key server. All undefined flags MUST be zero. This is found only on a + * self-signature. + * + * See [Key Server Preferences](https://tools.ietf.org/html/rfc4880#section-5.2.3.17) + */ + keyServerPreferences(23), + + /** + * This is a URI of a key server that the keyholder prefers be used for updates. Note that keys + * with multiple User IDs can have a preferred key server for each User ID. Note also that since + * this is a URI, the key server can actually be a copy of the key retrieved by ftp, http, + * finger, etc. + * + * See [Preferred Key Server](https://tools.ietf.org/html/rfc4880#section-5.2.3.18) + */ + preferredKeyServers(24), + + /** + * This is a flag in a User ID's self-signature that states whether this User ID is the main + * User ID for this key. It is reasonable for an implementation to resolve ambiguities in + * preferences, etc. by referring to the primary User ID. If this flag is absent, its value is + * zero. If more than one User ID in a key is marked as primary, the implementation may resolve + * the ambiguity in any way it sees fit, but it is RECOMMENDED that priority be given to the + * User ID with the most recent self-signature. + * + * When appearing on a self-signature on a User ID packet, this subpacket applies only to User + * ID packets. When appearing on a self-signature on a User Attribute packet, this subpacket + * applies only to User Attribute packets. That is to say, there are two different and + * independent "primaries" -- one for User IDs, and one for User Attributes. + * + * See [Primary User-ID](https://tools.ietf.org/html/rfc4880#section-5.2.3.19) + */ + primaryUserId(25), + + /** + * This subpacket contains a URI of a document that describes the policy under which the + * signature was issued. + * + * See [Policy URL](https://tools.ietf.org/html/rfc4880#section-5.2.3.20) + */ + policyUrl(26), + + /** + * This subpacket contains a list of binary flags that hold information about a key. It is a + * string of octets, and an implementation MUST NOT assume a fixed size. This is so it can grow + * over time. If a list is shorter than an implementation expects, the unstated flags are + * considered to be zero. + * + * See [Key Flags](https://tools.ietf.org/html/rfc4880#section-5.2.3.21) + */ + keyFlags(27), + + /** + * This subpacket allows a keyholder to state which User ID is responsible for the signing. Many + * keyholders use a single key for different purposes, such as business communications as well + * as personal communications. This subpacket allows such a keyholder to state which of their + * roles is making a signature. + * + * See [Signer's User ID](https://tools.ietf.org/html/rfc4880#section-5.2.3.22) + */ + signerUserId(28), + + /** + * This subpacket is used only in key revocation and certification revocation signatures. It + * describes the reason why the key or certificate was revoked. + * + * The first octet contains a machine-readable code that denotes the reason for the revocation: + * + * 0 - No reason specified (key revocations or cert revocations) + * 1 - Key is superseded (key revocations) + * 2 - Key material has been compromised (key revocations) + * 3 - Key is retired and no longer used (key revocations) + * 32 - User ID information is no longer valid (cert revocations) + * 100-110 - Private Use + * + * See [Reason for Revocation](https://tools.ietf.org/html/rfc4880#section-5.2.3.23) + */ + revocationReason(29), + + /** + * The Features subpacket denotes which advanced OpenPGP features a user's implementation + * supports. This is so that as features are added to OpenPGP that cannot be + * backwards-compatible, a user can state that they can use that feature. The flags are single + * bits that indicate that a given feature is supported. + * + * This subpacket is similar to a preferences subpacket, and only appears in a self-signature. + * + * See [Features](https://tools.ietf.org/html/rfc4880#section-5.2.3.24) + */ + features(30), + + /** + * This subpacket identifies a specific target signature to which a signature refers. For + * revocation signatures, this subpacket provides explicit designation of which signature is + * being revoked. For a third-party or timestamp signature, this designates what signature is + * signed. All arguments are an identifier of that target signature. + * + * The N octets of hash data MUST be the size of the hash of the signature. For example, a + * target signature with a SHA-1 hash MUST have 20 octets of hash data. + * + * See [Signature Target](https://tools.ietf.org/html/rfc4880#section-5.2.3.25) + */ + signatureTarget(31), + + /** + * This subpacket contains a complete Signature packet body as specified in Section 5.2 above. + * It is useful when one signature needs to refer to, or be incorporated in, another signature. + * + * See [Embedded Signature](https://tools.ietf.org/html/rfc4880#section-5.2.3.26) + */ + embeddedSignature(32), + + /** + * The OpenPGP Key fingerprint of the key issuing the signature. This subpacket SHOULD be + * included in all signatures. If the version of the issuing key is 4 and an Issuer subpacket is + * also included in the signature, the key ID of the Issuer subpacket MUST match the low 64 bits + * of the fingerprint. + * + * Note that the length N of the fingerprint for a version 4 key is 20 octets; for a version 5 + * key N is 32. + * + * See + * [Issuer Fingerprint](https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-10#section-5.2.3.28) + */ + issuerFingerprint(33), + + /** + * AEAD algorithm numbers that indicate which AEAD algorithms the keyholder prefers to use. The + * subpackets body is an ordered list of octets with the most preferred listed first. It is + * assumed that only algorithms listed are supported by the recipient's software. This is only + * found on a self-signature. Note that support for the AEAD Encrypted Data packet in the + * general is indicated by a Feature Flag. + * + * See + * [Preferred AEAD Algorithms](https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-10#section-5.2.3.8) + */ + preferredAEADAlgorithms(39), + + /** + * The OpenPGP Key fingerprint of the intended recipient primary key. If one or more subpackets + * of this type are included in a signature, it SHOULD be considered valid only in an encrypted + * context, where the key it was encrypted to is one of the indicated primary keys, or one of + * their subkeys. This can be used to prevent forwarding a signature outside its intended, + * encrypted context. + * + * Note that the length N of the fingerprint for a version 4 key is 20 octets; for a version 5 + * key N is 32. + * + * See + * [Intended Recipient Fingerprint](https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-10#section-5.2.3.29) + */ + intendedRecipientFingerprint(35), + + /** + * This subpacket MUST only appear as a hashed subpacket of an Attestation Key Signature. It has + * no meaning in any other signature type. It is used by the primary key to attest to a set of + * third- party certifications over the associated User ID or User Attribute. This enables the + * holder of an OpenPGP primary key to mark specific third-party certifications as + * re-distributable with the rest of the Transferable Public Key (see the "No-modify" flag in + * "Key Server Preferences", above). Implementations MUST include exactly one Attested + * Certification subpacket in any generated Attestation Key Signature. + * + * See + * [Attested Certification](https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-10#section-5.2.3.30) + */ + attestedCertification(37); + + companion object { + + /** + * Return the [SignatureSubpacket] that corresponds to the provided id. If an unmatched code + * is presented, return null. + * + * @param code id + * @return signature subpacket + */ + @JvmStatic + fun fromCode(code: Int): SignatureSubpacket? { + return values().firstOrNull { it.code == code } + } + + /** + * Return the [SignatureSubpacket] that corresponds to the provided code. + * + * @param code code + * @return signature subpacket + * @throws NoSuchElementException in case of an unmatched subpacket tag + */ + @JvmStatic + fun requireFromCode(code: Int): SignatureSubpacket { + return fromCode(code) + ?: throw NoSuchElementException("No SignatureSubpacket tag found with code $code") + } + + /** + * Convert an array of signature subpacket tags into a list of + * [SignatureSubpacket SignatureSubpackets]. + * + * @param codes array of codes + * @return list of subpackets + */ + @JvmStatic + fun fromCodes(vararg codes: Int): List { + return codes.toList().mapNotNull { fromCode(it) } + } + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/SignatureType.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/SignatureType.kt new file mode 100644 index 00000000..be6917df --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/SignatureType.kt @@ -0,0 +1,199 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.algorithm + +import org.bouncycastle.openpgp.PGPSignature + +/** + * Enum that enlists all the Signature Types defined in rfc4880 section 5.2.1 See [PGPSignature] for + * comparison. + * + * See [rfc4880 §5.2.1. Signature Types](https://tools.ietf.org/html/rfc4880#section-5.11) + */ +enum class SignatureType(val code: Int) { + + /** + * Signature of a binary document. This means the signer owns it, created it, or certifies that + * it has not been modified. + */ + BINARY_DOCUMENT(0x00), + + /** + * Signature of a canonical text document. This means the signer owns it, created it, or + * certifies that it has not been modified. The signature is calculated over the text data with + * its line endings converted to ``. + */ + CANONICAL_TEXT_DOCUMENT(0x01), + + /** + * Standalone signature. This signature is a signature of only its own subpacket contents. It is + * calculated identically to a signature over a zero-length binary document. Note that it + * doesn't make sense to have a V3 standalone signature. + */ + STANDALONE(0x02), + + /** + * Generic certification of a User ID and Public-Key packet. The issuer of this certification + * does not make any particular assertion as to how well the certifier has checked that the + * owner of the key is in fact the person described by the User ID. + */ + GENERIC_CERTIFICATION(0x10), + + /** + * Persona certification of a User ID and Public-Key packet. The issuer of this certification + * has not done any verification of the claim that the owner of this key is the User ID + * specified. + */ + NO_CERTIFICATION(0x11), + + /** + * Casual certification of a User ID and Public-Key packet. The issuer of this certification has + * done some casual verification of the claim of identity. + */ + CASUAL_CERTIFICATION(0x12), + + /** + * Positive certification of a User ID and Public-Key packet. The issuer of this certification + * has done substantial verification of the claim of identity. + */ + POSITIVE_CERTIFICATION(0x13), + + /** + * Subkey Binding Signature. This signature is a statement by the top-level signing key that + * indicates that it owns the subkey. This signature is calculated directly on the primary key + * and subkey, and not on any User ID or other packets. A signature that binds a signing subkey + * MUST have an Embedded Signature subpacket in this binding signature that contains a + * [#PRIMARYKEY_BINDING] signature made by the signing subkey on the primary key and subkey. + */ + SUBKEY_BINDING(0x18), + + /** + * Primary Key Binding Signature This signature is a statement by a signing subkey, indicating + * that it is owned by the primary key and subkey. This signature is calculated the same way as + * a [#SUBKEY_BINDING] signature: directly on the primary key and subkey, and not on any User ID + * or other packets. + */ + PRIMARYKEY_BINDING(0x19), + + /** + * Signature directly on a key This signature is calculated directly on a key. It binds the + * information in the Signature subpackets to the key, and is appropriate to be used for + * subpackets that provide information about the key, such as the Revocation Key subpacket. It + * is also appropriate for statements that non-self certifiers want to make about the key + * itself, rather than the binding between a key and a name. + */ + DIRECT_KEY(0x1f), + + /** + * Key revocation signature The signature is calculated directly on the key being revoked. A + * revoked key is not to be used. Only revocation signatures by the key being revoked, or by an + * authorized revocation key, should be considered valid revocation signatures. + */ + KEY_REVOCATION(0x20), + + /** + * Subkey revocation signature The signature is calculated directly on the subkey being revoked. + * A revoked subkey is not to be used. Only revocation signatures by the top-level signature key + * that is bound to this subkey, or by an authorized revocation key, should be considered valid + * revocation signatures. + */ + SUBKEY_REVOCATION(0x28), + + /** + * Certification revocation signature This signature revokes an earlier User ID certification + * signature (signature class 0x10 through 0x13) or signature [#DIRECT_KEY]. It should be issued + * by the same key that issued the revoked signature or an authorized revocation key. The + * signature is computed over the same data as the certificate that it revokes, and should have + * a later creation date than that certificate. + */ + CERTIFICATION_REVOCATION(0x30), + + /** Timestamp signature. This signature is only meaningful for the timestamp contained in it. */ + TIMESTAMP(0x40), + + /** + * Third-Party Confirmation signature. This signature is a signature over some other OpenPGP + * Signature packet(s). It is analogous to a notary seal on the signed data. A third-party + * signature SHOULD include Signature Target subpacket(s) to give easy identification. Note that + * we really do mean SHOULD. There are plausible uses for this (such as a blind party that only + * sees the signature, not the key or source document) that cannot include a target subpacket. + */ + THIRD_PARTY_CONFIRMATION(0x50); + + companion object { + + /** + * Convert a numerical id into a [SignatureType]. + * + * @param code numeric id + * @return signature type enum + */ + @JvmStatic + fun fromCode(code: Int): SignatureType? { + return values().firstOrNull { it.code == code } + } + + /** + * Convert a numerical id into a [SignatureType]. + * + * @param code numeric id + * @return signature type enum + * @throws NoSuchElementException in case of an unmatched signature type code + */ + @JvmStatic + fun requireFromCode(code: Int): SignatureType { + return fromCode(code) + ?: throw NoSuchElementException( + "Signature type 0x${Integer.toHexString(code)} appears to be invalid.") + } + + /** + * Convert a numerical id into a [SignatureType]. + * + * @param code numeric id + * @return signature type enum + * @throws IllegalArgumentException in case of an unmatched signature type code + */ + @JvmStatic + @Deprecated( + "Deprecated in favor of requireFromCode", + ReplaceWith("SignatureType.requireFromCode(code)")) + fun valueOf(code: Int): SignatureType { + try { + return requireFromCode(code) + } catch (e: NoSuchElementException) { + throw IllegalArgumentException(e.message) + } + } + + @JvmStatic + fun isRevocationSignature(signatureType: Int): Boolean { + val sigType = fromCode(signatureType) + return sigType?.let { isRevocationSignature(it) } ?: false + } + + @JvmStatic + fun isRevocationSignature(signatureType: SignatureType): Boolean { + return when (signatureType) { + BINARY_DOCUMENT, + CANONICAL_TEXT_DOCUMENT, + STANDALONE, + GENERIC_CERTIFICATION, + NO_CERTIFICATION, + CASUAL_CERTIFICATION, + POSITIVE_CERTIFICATION, + SUBKEY_BINDING, + PRIMARYKEY_BINDING, + DIRECT_KEY, + TIMESTAMP, + THIRD_PARTY_CONFIRMATION -> false + KEY_REVOCATION, + SUBKEY_REVOCATION, + CERTIFICATION_REVOCATION -> true + else -> throw IllegalArgumentException("Unknown signature type: $signatureType") + } + } + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/StreamEncoding.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/StreamEncoding.kt new file mode 100644 index 00000000..74b2a56b --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/StreamEncoding.kt @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.algorithm + +/** + * Enumeration of possible encoding formats of the content of the literal data packet. + * + * See [RFC4880: Literal Data Packet](https://tools.ietf.org/html/rfc4880#section-5.9) + */ +enum class StreamEncoding(val code: Char) { + + /** The Literal packet contains binary data. */ + BINARY('b'), + + /** + * The Literal packet contains text data, and thus may need line ends converted to local form, + * or other text-mode changes. + */ + TEXT('t'), + + /** Indication that the implementation believes that the literal data contains UTF-8 text. */ + UTF8('u'), + + /** + * Early versions of PGP also defined a value of 'l' as a 'local' mode for machine-local + * conversions. RFC 1991 [RFC1991] incorrectly stated this local mode flag as '1' (ASCII numeral + * one). Both of these local modes are deprecated. + */ + @Deprecated("LOCAL is deprecated.") LOCAL('l'), + ; + + companion object { + /** + * Return the [StreamEncoding] corresponding to the provided code identifier. If no matching + * encoding is found, return null. + * + * @param code identifier + * @return encoding enum + */ + @JvmStatic + fun fromCode(code: Int): StreamEncoding? { + return values().firstOrNull { it.code == code.toChar() } + ?: if (code == 1) return LOCAL else null + } + + /** + * Return the [StreamEncoding] corresponding to the provided code identifier. If no matching + * encoding is found, throw a [NoSuchElementException]. + * + * @param code identifier + * @return encoding enum + * @throws NoSuchElementException in case of an unmatched identifier + */ + @JvmStatic + fun requireFromCode(code: Int): StreamEncoding { + return fromCode(code) + ?: throw NoSuchElementException("No StreamEncoding found for code $code") + } + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/SymmetricKeyAlgorithm.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/SymmetricKeyAlgorithm.kt new file mode 100644 index 00000000..bfd32343 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/SymmetricKeyAlgorithm.kt @@ -0,0 +1,89 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.algorithm + +/** + * Enumeration of possible symmetric encryption algorithms. + * + * See [RFC4880: Symmetric-Key Algorithms](https://tools.ietf.org/html/rfc4880#section-9.2) + */ +enum class SymmetricKeyAlgorithm(val algorithmId: Int) { + + /** Plaintext or unencrypted data. */ + NULL(0), + + /** + * IDEA is deprecated. + * + * @deprecated use a different algorithm. + */ + @Deprecated("IDEA is deprecated.") IDEA(1), + + /** TripleDES (DES-EDE - 168 bit key derived from 192). */ + TRIPLE_DES(2), + + /** CAST5 (128-bit key, as per RFC2144). */ + CAST5(3), + + /** Blowfish (128-bit key, 16 rounds). */ + BLOWFISH(4), + + /** Reserved in RFC4880. SAFER-SK128 (13 rounds) */ + SAFER(5), + + /** Reserved in RFC4880. Reserved for DES/SK */ + DES(6), + + /** AES with 128-bit key. */ + AES_128(7), + + /** AES with 192-bit key. */ + AES_192(8), + + /** AES with 256-bit key. */ + AES_256(9), + + /** Twofish with 256-bit key. */ + TWOFISH(10), + + /** Reserved for Camellia with 128-bit key. */ + CAMELLIA_128(11), + + /** Reserved for Camellia with 192-bit key. */ + CAMELLIA_192(12), + + /** Reserved for Camellia with 256-bit key. */ + CAMELLIA_256(13), + ; + + companion object { + + /** + * Return the [SymmetricKeyAlgorithm] enum that corresponds to the provided numeric id. If + * an invalid id is provided, null is returned. + * + * @param id numeric algorithm id + * @return symmetric key algorithm enum + */ + @JvmStatic + fun fromId(id: Int): SymmetricKeyAlgorithm? { + return values().firstOrNull { it.algorithmId == id } + } + + /** + * Return the [SymmetricKeyAlgorithm] enum that corresponds to the provided numeric id. If + * an invalid id is provided, throw a [NoSuchElementException]. + * + * @param id numeric algorithm id + * @return symmetric key algorithm enum + * @throws NoSuchElementException if an unmatched id is provided + */ + @JvmStatic + fun requireFromId(id: Int): SymmetricKeyAlgorithm { + return fromId(id) + ?: throw NoSuchElementException("No SymmetricKeyAlgorithm found for id $id") + } + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/Trustworthiness.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/Trustworthiness.kt new file mode 100644 index 00000000..5b9f4c40 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/Trustworthiness.kt @@ -0,0 +1,138 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.algorithm + +/** + * Facade class for [org.bouncycastle.bcpg.sig.TrustSignature]. A trust signature subpacket marks + * the trustworthiness of a certificate and defines its capabilities to act as a trusted introducer. + */ +class Trustworthiness(amount: Int, depth: Int) { + val depth = capDepth(depth) + val amount = capAmount(amount) + + /** + * Returns true, if the trust amount is equal to 0. This means the key is not trusted. + * + * Otherwise return false + * + * @return true if untrusted + */ + fun isNotTrusted() = amount == NOT_TRUSTED + + /** + * Return true if the certificate is at least marginally trusted. That is the case, if the trust + * amount is greater than 0. + * + * @return true if the cert is at least marginally trusted + */ + fun isMarginallyTrusted() = amount > NOT_TRUSTED + + /** + * Return true if the certificate is fully trusted. That is the case if the trust amount is + * greater than or equal to 120. + * + * @return true if the cert is fully trusted + */ + fun isFullyTrusted() = amount >= THRESHOLD_FULLY_CONVINCED + + /** + * Return true, if the cert is an introducer. That is the case if the depth is greater 0. + * + * @return true if introducer + */ + fun isIntroducer() = depth >= 1 + + /** + * Return true, if the certified cert can introduce certificates with trust depth of + *

otherDepth
. + * + * @param otherDepth other certifications trust depth + * @return true if the cert can introduce the other + */ + fun canIntroduce(otherDepth: Int) = depth > otherDepth + + /** + * Return true, if the certified cert can introduce certificates with the given
other
+ * trust depth. + * + * @param other other certificates trust depth + * @return true if the cert can introduce the other + */ + fun canIntroduce(other: Trustworthiness) = canIntroduce(other.depth) + + companion object { + const val THRESHOLD_FULLY_CONVINCED = 120 // greater or equal is fully trusted + const val MARGINALLY_CONVINCED = 60 // default value for marginally convinced + const val NOT_TRUSTED = 0 // 0 is not trusted + + @JvmStatic private val validRange = 0..255 + + /** + * This means that we are fully convinced of the trustworthiness of the key. + * + * @return builder + */ + @JvmStatic fun fullyTrusted() = Builder(THRESHOLD_FULLY_CONVINCED) + + /** + * This means that we are marginally (partially) convinced of the trustworthiness of the + * key. + * + * @return builder + */ + @JvmStatic fun marginallyTrusted() = Builder(MARGINALLY_CONVINCED) + + /** + * This means that we do not trust the key. Can be used to overwrite previous trust. + * + * @return builder + */ + @JvmStatic fun untrusted() = Builder(NOT_TRUSTED) + + @JvmStatic + private fun capAmount(amount: Int): Int { + if (amount !in validRange) { + throw IllegalArgumentException("Trust amount MUST be a value between 0 and 255") + } + return amount + } + + @JvmStatic + private fun capDepth(depth: Int): Int { + if (depth !in validRange) { + throw IllegalArgumentException("Trust depth MUST be a value between 0 and 255") + } + return depth + } + } + + class Builder(val amount: Int) { + + /** + * The key is a trusted introducer (depth 1). Certifications made by this key are considered + * trustworthy. + * + * @return trust + */ + fun introducer() = Trustworthiness(amount, 1) + + /** + * The key is a meta introducer (depth 2). This key can introduce trusted introducers of + * depth 1. + * + * @return trust + */ + fun metaIntroducer() = Trustworthiness(amount, 2) + + /** + * The key is a meta introducer of depth
n
. This key can introduce meta + * introducers of depth
n - 1
. + * + * @param n depth + * @return trust + */ + fun metaIntroducerOfDepth(d: Int) = Trustworthiness(amount, d) + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/negotiation/HashAlgorithmNegotiator.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/negotiation/HashAlgorithmNegotiator.kt new file mode 100644 index 00000000..b9474247 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/negotiation/HashAlgorithmNegotiator.kt @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.algorithm.negotiation + +import org.pgpainless.algorithm.HashAlgorithm +import org.pgpainless.policy.Policy + +/** + * Interface for a class that negotiates [HashAlgorithms][HashAlgorithm]. + * + * You can provide your own implementation using custom logic by implementing the + * [negotiateHashAlgorithm(Set)] method. + */ +interface HashAlgorithmNegotiator { + + /** + * Pick one [HashAlgorithm] from the ordered set of acceptable algorithms. + * + * @param orderedPrefs hash algorithm preferences + * @return picked algorithms + */ + fun negotiateHashAlgorithm(orderedPrefs: Set): HashAlgorithm + + companion object { + + /** + * Return an instance that negotiates [HashAlgorithms][HashAlgorithm] used for + * non-revocation signatures based on the given [Policy]. + * + * @param policy algorithm policy + * @return negotiator + */ + @JvmStatic + fun negotiateSignatureHashAlgorithm(policy: Policy): HashAlgorithmNegotiator { + return negotiateByPolicy(policy.dataSignatureHashAlgorithmPolicy) + } + + /** + * Return an instance that negotiates [HashAlgorithms][HashAlgorithm] used for revocation + * signatures based on the given [Policy]. + * + * @param policy algorithm policy + * @return negotiator + */ + @JvmStatic + fun negotiateRevocationSignatureAlgorithm(policy: Policy): HashAlgorithmNegotiator { + return negotiateByPolicy(policy.revocationSignatureHashAlgorithmPolicy) + } + + /** + * Return an instance that negotiates [HashAlgorithms][HashAlgorithm] based on the given + * [Policy.HashAlgorithmPolicy]. + * + * @param hashAlgorithmPolicy algorithm policy for hash algorithms + * @return negotiator + */ + @JvmStatic + fun negotiateByPolicy( + hashAlgorithmPolicy: Policy.HashAlgorithmPolicy + ): HashAlgorithmNegotiator { + return object : HashAlgorithmNegotiator { + override fun negotiateHashAlgorithm( + orderedPrefs: Set + ): HashAlgorithm { + return orderedPrefs.firstOrNull { hashAlgorithmPolicy.isAcceptable(it) } + ?: hashAlgorithmPolicy.defaultHashAlgorithm() + } + } + } + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/negotiation/SymmetricKeyAlgorithmNegotiator.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/negotiation/SymmetricKeyAlgorithmNegotiator.kt new file mode 100644 index 00000000..d11f2c03 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/negotiation/SymmetricKeyAlgorithmNegotiator.kt @@ -0,0 +1,81 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.algorithm.negotiation + +import java.lang.IllegalArgumentException +import org.pgpainless.algorithm.SymmetricKeyAlgorithm +import org.pgpainless.policy.Policy + +interface SymmetricKeyAlgorithmNegotiator { + + /** + * Negotiate a symmetric encryption algorithm. If the override is non-null, it will be returned + * instead of performing an actual negotiation. Otherwise, the list of ordered sets containing + * the preferences of different recipient keys will be used to determine a suitable symmetric + * encryption algorithm. + * + * @param policy algorithm policy + * @param override algorithm override (if not null, return this) + * @param keyPreferences list of preferences per key + * @return negotiated algorithm + */ + fun negotiate( + policy: Policy.SymmetricKeyAlgorithmPolicy, + override: SymmetricKeyAlgorithm?, + keyPreferences: List> + ): SymmetricKeyAlgorithm + + companion object { + @JvmStatic + fun byPopularity(): SymmetricKeyAlgorithmNegotiator { + return object : SymmetricKeyAlgorithmNegotiator { + override fun negotiate( + policy: Policy.SymmetricKeyAlgorithmPolicy, + override: SymmetricKeyAlgorithm?, + keyPreferences: List> + ): SymmetricKeyAlgorithm { + if (override == SymmetricKeyAlgorithm.NULL) { + throw IllegalArgumentException( + "Algorithm override cannot be NULL (plaintext).") + } + + if (override != null) { + return override + } + + // algorithm to #occurrences + val supportWeight = buildMap { + keyPreferences.forEach { keyPreference -> + keyPreference.forEach { pref -> + put(pref, getOrDefault(pref, 0) as Int + 1) + } + } + } + + // Pivot map and sort by popularity ascending + // score to list(algo) + val byScore = + supportWeight + .toList() + .map { e -> e.second to e.first } + .groupBy { e -> e.first } + .map { e -> e.key to e.value.map { it.second }.toList() } + .associate { e -> e } + .toSortedMap() + + // iterate in reverse over algorithms + for (e in byScore.entries.reversed()) { + val best = policy.selectBest(e.value) + if (best != null) { + return best + } + } + + return policy.defaultSymmetricKeyAlgorithm + } + } + } + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/authentication/CertificateAuthenticity.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/authentication/CertificateAuthenticity.kt new file mode 100644 index 00000000..f3d60bf6 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/authentication/CertificateAuthenticity.kt @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.authentication + +import org.bouncycastle.openpgp.PGPPublicKeyRing + +class CertificateAuthenticity( + val userId: String, + val certificate: PGPPublicKeyRing, + val certificationChains: Map, + val targetAmount: Int +) { + + val totalTrustAmount: Int + get() = certificationChains.values.sum() + + /** + * Return the degree of authentication of the binding in percent. 100% means full + * authentication. Values smaller than 100% mean partial authentication. + * + * @return authenticity in percent + */ + val authenticityPercentage: Int + get() = targetAmount * 100 / totalTrustAmount + + /** + * Return true, if the binding is authenticated to a sufficient degree. + * + * @return true if total gathered evidence outweighs the target trust amount. + */ + val authenticated: Boolean + get() = targetAmount <= totalTrustAmount + + fun isAuthenticated() = authenticated +} + +/** + * A chain of certifications. + * + * @param trustAmount actual trust amount of the chain + * @param chainLinks links of the chain, starting at the trust-root, ending at the target. + */ +class CertificationChain(val trustAmount: Int, val chainLinks: List) {} + +/** A chain link contains a node in the trust chain. */ +class ChainLink(val certificate: PGPPublicKeyRing) {} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/authentication/CertificateAuthority.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/authentication/CertificateAuthority.kt new file mode 100644 index 00000000..093c2325 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/authentication/CertificateAuthority.kt @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.authentication + +import java.util.* +import org.pgpainless.key.OpenPgpFingerprint + +/** + * Interface for a CA that can authenticate trust-worthy certificates. Such a CA might be a fixed + * list of trustworthy certificates, or a dynamic implementation like the Web-of-Trust. + * + * @see PGPainless-WOT + * @see OpenPGP Web of Trust + */ +interface CertificateAuthority { + + /** + * Determine the authenticity of the binding between the given fingerprint and the userId. In + * other words, determine, how much evidence can be gathered, that the certificate with the + * given fingerprint really belongs to the user with the given userId. + * + * @param fingerprint fingerprint of the certificate + * @param userId userId + * @param email if true, the userId will be treated as an email address and all user-IDs + * containing the email address will be matched. + * @param referenceTime reference time at which the binding shall be evaluated + * @param targetAmount target trust amount (120 = fully authenticated, 240 = doubly + * authenticated, 60 = partially authenticated...) + * @return information about the authenticity of the binding + */ + fun authenticateBinding( + fingerprint: OpenPgpFingerprint, + userId: String, + email: Boolean, + referenceTime: Date, + targetAmount: Int + ): CertificateAuthenticity + + /** + * Lookup certificates, which carry a trustworthy binding to the given userId. + * + * @param userId userId + * @param email if true, the user-ID will be treated as an email address and all user-IDs + * containing the email address will be matched. + * @param referenceTime reference time at which the binding shall be evaluated + * @param targetAmount target trust amount (120 = fully authenticated, 240 = doubly + * authenticated, 60 = partially authenticated...) + * @return list of identified bindings + */ + fun lookupByUserId( + userId: String, + email: Boolean, + referenceTime: Date, + targetAmount: Int + ): List + + /** + * Identify trustworthy bindings for a certificate. The result is a list of authenticatable + * userIds on the certificate. + * + * @param fingerprint fingerprint of the certificate + * @param referenceTime reference time for trust calculations + * @param targetAmount target trust amount (120 = fully authenticated, 240 = doubly + * authenticated, 60 = partially authenticated...) + * @return list of identified bindings + */ + fun identifyByFingerprint( + fingerprint: OpenPgpFingerprint, + referenceTime: Date, + targetAmount: Int + ): List +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/PGPKeyRingExtensions.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/PGPKeyRingExtensions.kt new file mode 100644 index 00000000..7126db66 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/PGPKeyRingExtensions.kt @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.bouncycastle.extensions + +import openpgp.openPgpKeyId +import org.bouncycastle.openpgp.PGPKeyRing +import org.bouncycastle.openpgp.PGPOnePassSignature +import org.bouncycastle.openpgp.PGPPublicKey +import org.bouncycastle.openpgp.PGPSignature +import org.pgpainless.PGPainless +import org.pgpainless.key.OpenPgpFingerprint +import org.pgpainless.key.SubkeyIdentifier + +/** Return true, if this [PGPKeyRing] contains the subkey identified by the [SubkeyIdentifier]. */ +fun PGPKeyRing.matches(subkeyIdentifier: SubkeyIdentifier): Boolean = + this.publicKey.keyID == subkeyIdentifier.primaryKeyId && + this.getPublicKey(subkeyIdentifier.subkeyId) != null + +/** + * Return true, if the [PGPKeyRing] contains a public key with the given key-ID. + * + * @param keyId keyId + * @return true if key with the given key-ID is present, false otherwise + */ +fun PGPKeyRing.hasPublicKey(keyId: Long): Boolean = this.getPublicKey(keyId) != null + +/** + * Return true, if the [PGPKeyRing] contains a public key with the given fingerprint. + * + * @param fingerprint fingerprint + * @return true if key with the given fingerprint is present, false otherwise + */ +fun PGPKeyRing.hasPublicKey(fingerprint: OpenPgpFingerprint): Boolean = + this.getPublicKey(fingerprint) != null + +/** + * Return the [PGPPublicKey] with the given [OpenPgpFingerprint] or null, if no such key is present. + * + * @param fingerprint fingerprint + * @return public key + */ +fun PGPKeyRing.getPublicKey(fingerprint: OpenPgpFingerprint): PGPPublicKey? = + this.getPublicKey(fingerprint.bytes) + +fun PGPKeyRing.requirePublicKey(keyId: Long): PGPPublicKey = + getPublicKey(keyId) + ?: throw NoSuchElementException( + "OpenPGP key does not contain key with id ${keyId.openPgpKeyId()}.") + +fun PGPKeyRing.requirePublicKey(fingerprint: OpenPgpFingerprint): PGPPublicKey = + getPublicKey(fingerprint) + ?: throw NoSuchElementException( + "OpenPGP key does not contain key with fingerprint $fingerprint.") + +/** + * Return the [PGPPublicKey] that matches the [OpenPgpFingerprint] of the given [PGPSignature]. If + * the [PGPSignature] does not carry an issuer-fingerprint subpacket, fall back to the issuer-keyID + * subpacket to identify the [PGPPublicKey] via its key-ID. + */ +fun PGPKeyRing.getPublicKeyFor(signature: PGPSignature): PGPPublicKey? = + signature.fingerprint?.let { this.getPublicKey(it) } ?: this.getPublicKey(signature.keyID) + +/** Return the [PGPPublicKey] that matches the key-ID of the given [PGPOnePassSignature] packet. */ +fun PGPKeyRing.getPublicKeyFor(onePassSignature: PGPOnePassSignature): PGPPublicKey? = + this.getPublicKey(onePassSignature.keyID) + +/** Return the [OpenPgpFingerprint] of this OpenPGP key. */ +val PGPKeyRing.openPgpFingerprint: OpenPgpFingerprint + get() = OpenPgpFingerprint.of(this) + +/** Return this OpenPGP key as an ASCII armored String. */ +fun PGPKeyRing.toAsciiArmor(): String = PGPainless.asciiArmor(this) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/PGPPublicKeyExtensions.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/PGPPublicKeyExtensions.kt new file mode 100644 index 00000000..d267fa83 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/PGPPublicKeyExtensions.kt @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.bouncycastle.extensions + +import org.bouncycastle.asn1.gnu.GNUObjectIdentifiers +import org.bouncycastle.bcpg.ECDHPublicBCPGKey +import org.bouncycastle.bcpg.ECDSAPublicBCPGKey +import org.bouncycastle.bcpg.EdDSAPublicBCPGKey +import org.bouncycastle.jcajce.provider.asymmetric.util.ECUtil +import org.bouncycastle.openpgp.PGPPublicKey +import org.pgpainless.algorithm.PublicKeyAlgorithm +import org.pgpainless.key.OpenPgpFingerprint +import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve + +/** + * For secret keys of types [PublicKeyAlgorithm.ECDSA], [PublicKeyAlgorithm.ECDH] and + * [PublicKeyAlgorithm.EDDSA_LEGACY], this method returns the name of the underlying elliptic curve. + * + * For other key types or unknown curves, this method throws an [IllegalArgumentException]. + * + * @return curve name + */ +fun PGPPublicKey.getCurveName(): String { + PublicKeyAlgorithm.requireFromId(algorithm) + .let { + when (it) { + PublicKeyAlgorithm.ECDSA -> publicKeyPacket.key as ECDSAPublicBCPGKey + PublicKeyAlgorithm.ECDH -> publicKeyPacket.key as ECDHPublicBCPGKey + PublicKeyAlgorithm.EDDSA_LEGACY -> publicKeyPacket.key as EdDSAPublicBCPGKey + else -> throw IllegalArgumentException("No an elliptic curve public key ($it).") + } + } + .let { + if (it.curveOID == GNUObjectIdentifiers.Ed25519) + return EdDSALegacyCurve._Ed25519.curveName + else it.curveOID + } + .let { it to ECUtil.getCurveName(it) } + .let { + if (it.second != null) return it.second + else throw IllegalArgumentException("Unknown curve: ${it.first}") + } +} + +/** Return the [PublicKeyAlgorithm] of this key. */ +val PGPPublicKey.publicKeyAlgorithm: PublicKeyAlgorithm + get() = PublicKeyAlgorithm.requireFromId(algorithm) + +/** Return the [OpenPgpFingerprint] of this key. */ +val PGPPublicKey.openPgpFingerprint: OpenPgpFingerprint + get() = OpenPgpFingerprint.of(this) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/PGPSecretKeyExtensions.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/PGPSecretKeyExtensions.kt new file mode 100644 index 00000000..84f81e64 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/PGPSecretKeyExtensions.kt @@ -0,0 +1,83 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.bouncycastle.extensions + +import org.bouncycastle.bcpg.S2K +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPPrivateKey +import org.bouncycastle.openpgp.PGPSecretKey +import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor +import org.pgpainless.algorithm.PublicKeyAlgorithm +import org.pgpainless.exception.KeyIntegrityException +import org.pgpainless.exception.WrongPassphraseException +import org.pgpainless.key.OpenPgpFingerprint +import org.pgpainless.key.protection.SecretKeyRingProtector +import org.pgpainless.key.protection.UnlockSecretKey +import org.pgpainless.util.Passphrase + +/** + * Unlock the secret key to get its [PGPPrivateKey]. + * + * @param passphrase passphrase to unlock the secret key with. + * @throws PGPException if the key cannot be unlocked + * @throws KeyIntegrityException if the public key part was tampered with + * @throws WrongPassphraseException + */ +@Throws(PGPException::class, KeyIntegrityException::class) +fun PGPSecretKey.unlock(passphrase: Passphrase): PGPPrivateKey = + UnlockSecretKey.unlockSecretKey(this, passphrase) + +/** + * Unlock the secret key to get its [PGPPrivateKey]. + * + * @param protector protector to unlock the secret key. + * @throws PGPException if the key cannot be unlocked + * @throws KeyIntegrityException if the public key part was tampered with + */ +@Throws(PGPException::class, KeyIntegrityException::class) +@JvmOverloads +fun PGPSecretKey.unlock( + protector: SecretKeyRingProtector = SecretKeyRingProtector.unprotectedKeys() +): PGPPrivateKey = UnlockSecretKey.unlockSecretKey(this, protector) + +/** + * Unlock the secret key to get its [PGPPrivateKey]. + * + * @param decryptor decryptor to unlock the secret key. + * @throws PGPException if the key cannot be unlocked + * @throws KeyIntegrityException if the public key part was tampered with + */ +@Throws(PGPException::class, KeyIntegrityException::class) +fun PGPSecretKey.unlock(decryptor: PBESecretKeyDecryptor?): PGPPrivateKey = + UnlockSecretKey.unlockSecretKey(this, decryptor) + +/** + * Returns indication that the secret key is encrypted. + * + * @return true if secret key is encrypted, false otherwise. + */ +fun PGPSecretKey?.isEncrypted(): Boolean = (this != null) && (s2KUsage != 0) + +/** + * Returns indication that the secret key is not encrypted. + * + * @return true if secret key is encrypted, false otherwise. + */ +fun PGPSecretKey?.isDecrypted(): Boolean = (this == null) || (s2KUsage == 0) + +/** + * Returns indication that the secret key has S2K of a type GNU_DUMMY_S2K. + * + * @return true if secret key has S2K of type GNU_DUMMY_S2K, false otherwise. + */ +fun PGPSecretKey?.hasDummyS2K(): Boolean = (this != null) && (s2K?.type == S2K.GNU_DUMMY_S2K) + +/** Return the [PublicKeyAlgorithm] of this key. */ +val PGPSecretKey.publicKeyAlgorithm: PublicKeyAlgorithm + get() = publicKey.publicKeyAlgorithm + +/** Return the [OpenPgpFingerprint] of this key. */ +val PGPSecretKey.openPgpFingerprint: OpenPgpFingerprint + get() = OpenPgpFingerprint.of(this) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/PGPSecretKeyRingExtensions.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/PGPSecretKeyRingExtensions.kt new file mode 100644 index 00000000..99c562e6 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/PGPSecretKeyRingExtensions.kt @@ -0,0 +1,79 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.bouncycastle.extensions + +import openpgp.openPgpKeyId +import org.bouncycastle.openpgp.* +import org.pgpainless.key.OpenPgpFingerprint + +/** OpenPGP certificate containing the public keys of this OpenPGP key. */ +val PGPSecretKeyRing.certificate: PGPPublicKeyRing + get() = PGPPublicKeyRing(this.publicKeys.asSequence().toList()) + +/** + * Return true, if the [PGPSecretKeyRing] contains a [PGPSecretKey] with the given key-ID. + * + * @param keyId keyId of the secret key + * @return true, if the [PGPSecretKeyRing] has a matching [PGPSecretKey], false otherwise + */ +fun PGPSecretKeyRing.hasSecretKey(keyId: Long): Boolean = this.getSecretKey(keyId) != null + +/** + * Return true, if the [PGPSecretKeyRing] contains a [PGPSecretKey] with the given fingerprint. + * + * @param fingerprint fingerprint + * @return true, if the [PGPSecretKeyRing] has a matching [PGPSecretKey], false otherwise + */ +fun PGPSecretKeyRing.hasSecretKey(fingerprint: OpenPgpFingerprint): Boolean = + this.getSecretKey(fingerprint) != null + +/** + * Return the [PGPSecretKey] with the given [OpenPgpFingerprint]. + * + * @param fingerprint fingerprint of the secret key + * @return the secret key or null + */ +fun PGPSecretKeyRing.getSecretKey(fingerprint: OpenPgpFingerprint): PGPSecretKey? = + this.getSecretKey(fingerprint.bytes) + +/** + * Return the [PGPSecretKey] with the given key-ID. + * + * @throws NoSuchElementException if the OpenPGP key doesn't contain a secret key with the given + * key-ID + */ +fun PGPSecretKeyRing.requireSecretKey(keyId: Long): PGPSecretKey = + getSecretKey(keyId) + ?: throw NoSuchElementException( + "OpenPGP key does not contain key with id ${keyId.openPgpKeyId()}.") + +/** + * Return the [PGPSecretKey] with the given fingerprint. + * + * @throws NoSuchElementException of the OpenPGP key doesn't contain a secret key with the given + * fingerprint + */ +fun PGPSecretKeyRing.requireSecretKey(fingerprint: OpenPgpFingerprint): PGPSecretKey = + getSecretKey(fingerprint) + ?: throw NoSuchElementException( + "OpenPGP key does not contain key with fingerprint $fingerprint.") + +/** + * Return the [PGPSecretKey] that matches the [OpenPgpFingerprint] of the given [PGPSignature]. If + * the [PGPSignature] does not carry an issuer-fingerprint subpacket, fall back to the issuer-keyID + * subpacket to identify the [PGPSecretKey] via its key-ID. + */ +fun PGPSecretKeyRing.getSecretKeyFor(signature: PGPSignature): PGPSecretKey? = + signature.fingerprint?.let { this.getSecretKey(it) } ?: this.getSecretKey(signature.keyID) + +/** Return the [PGPSecretKey] that matches the key-ID of the given [PGPOnePassSignature] packet. */ +fun PGPSecretKeyRing.getSecretKeyFor(onePassSignature: PGPOnePassSignature): PGPSecretKey? = + this.getSecretKey(onePassSignature.keyID) + +fun PGPSecretKeyRing.getSecretKeyFor(pkesk: PGPPublicKeyEncryptedData): PGPSecretKey? = + when (pkesk.version) { + 3 -> this.getSecretKey(pkesk.keyID) + else -> throw NotImplementedError("Version 6 PKESKs are not yet supported.") + } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/PGPSignatureExtensions.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/PGPSignatureExtensions.kt new file mode 100644 index 00000000..1393883c --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/PGPSignatureExtensions.kt @@ -0,0 +1,108 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.bouncycastle.extensions + +import java.util.* +import openpgp.plusSeconds +import org.bouncycastle.openpgp.PGPPublicKey +import org.bouncycastle.openpgp.PGPSignature +import org.pgpainless.algorithm.HashAlgorithm +import org.pgpainless.algorithm.PublicKeyAlgorithm +import org.pgpainless.algorithm.RevocationState +import org.pgpainless.algorithm.SignatureType +import org.pgpainless.key.OpenPgpFingerprint +import org.pgpainless.key.util.RevocationAttributes.Reason +import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil + +/** + * Return the value of the KeyExpirationDate subpacket, or null, if the signature does not carry + * such a subpacket. + */ +fun PGPSignature.getKeyExpirationDate(keyCreationDate: Date): Date? = + SignatureSubpacketsUtil.getKeyExpirationTime(this)?.let { keyCreationDate.plusSeconds(it.time) } + +/** + * Return the value of the signature ExpirationTime subpacket, or null, if the signature does not + * carry such a subpacket. + */ +val PGPSignature.signatureExpirationDate: Date? + get() = + SignatureSubpacketsUtil.getSignatureExpirationTime(this)?.let { + this.creationTime.plusSeconds(it.time) + } + +/** Return true, if the signature is expired at the given reference time. */ +fun PGPSignature.isExpired(referenceTime: Date = Date()) = + signatureExpirationDate?.let { referenceTime >= it } ?: false + +/** + * Return the key-ID of the issuer, determined by examining the IssuerKeyId and IssuerFingerprint + * subpackets of the signature. + */ +val PGPSignature.issuerKeyId: Long + get() = + when (version) { + 2, + 3 -> keyID + else -> { + SignatureSubpacketsUtil.getIssuerKeyIdAsLong(this)?.let { + if (it != 0L) it else null + } + ?: fingerprint?.keyId ?: 0L + } + } + +/** Return true, if the signature was likely issued by a key with the given fingerprint. */ +fun PGPSignature.wasIssuedBy(fingerprint: OpenPgpFingerprint): Boolean = + this.fingerprint?.let { it.keyId == fingerprint.keyId } ?: (keyID == fingerprint.keyId) + +/** + * Return true, if the signature was likely issued by a key with the given fingerprint. + * + * @param fingerprint fingerprint bytes + */ +@Deprecated("Discouraged in favor of method taking an OpenPgpFingerprint.") +fun PGPSignature.wasIssuedBy(fingerprint: ByteArray): Boolean = + try { + wasIssuedBy(OpenPgpFingerprint.parseFromBinary(fingerprint)) + } catch (e: IllegalArgumentException) { + // Unknown fingerprint length / format + false + } + +fun PGPSignature.wasIssuedBy(key: PGPPublicKey): Boolean = wasIssuedBy(OpenPgpFingerprint.of(key)) + +/** Return true, if this signature is a hard revocation. */ +val PGPSignature.isHardRevocation + get() = + when (SignatureType.fromCode(signatureType)) { + null -> false + SignatureType.KEY_REVOCATION, + SignatureType.SUBKEY_REVOCATION, + SignatureType.CERTIFICATION_REVOCATION -> { + SignatureSubpacketsUtil.getRevocationReason(this)?.let { + Reason.isHardRevocation(it.revocationReason) + } + ?: true // no reason -> hard revocation + } + else -> false // Not a revocation + } + +fun PGPSignature?.toRevocationState() = + if (this == null) RevocationState.notRevoked() + else if (isHardRevocation) RevocationState.hardRevoked() + else RevocationState.softRevoked(creationTime) + +val PGPSignature.fingerprint: OpenPgpFingerprint? + get() = SignatureSubpacketsUtil.getIssuerFingerprintAsOpenPgpFingerprint(this) + +val PGPSignature.publicKeyAlgorithm: PublicKeyAlgorithm + get() = PublicKeyAlgorithm.requireFromId(keyAlgorithm) + +val PGPSignature.signatureHashAlgorithm: HashAlgorithm + get() = HashAlgorithm.requireFromId(hashAlgorithm) + +fun PGPSignature.isOfType(type: SignatureType): Boolean = + SignatureType.fromCode(signatureType) == type diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/CachingBcPublicKeyDataDecryptorFactory.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/CachingBcPublicKeyDataDecryptorFactory.kt new file mode 100644 index 00000000..9bafa6da --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/CachingBcPublicKeyDataDecryptorFactory.kt @@ -0,0 +1,78 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification + +import org.bouncycastle.bcpg.AEADEncDataPacket +import org.bouncycastle.bcpg.SymmetricEncIntegrityPacket +import org.bouncycastle.openpgp.PGPPrivateKey +import org.bouncycastle.openpgp.PGPSessionKey +import org.bouncycastle.openpgp.operator.PGPDataDecryptor +import org.bouncycastle.openpgp.operator.bc.BcPublicKeyDataDecryptorFactory +import org.bouncycastle.util.encoders.Base64 +import org.pgpainless.key.SubkeyIdentifier + +/** + * Implementation of the [PublicKeyDataDecryptorFactory] which caches decrypted session keys. That + * way, if a message needs to be decrypted multiple times, expensive private key operations can be + * omitted. + * + * This implementation changes the behavior or [recoverSessionData] to first return any cache hits. + * If no hit is found, the method call is delegated to the underlying + * [PublicKeyDataDecryptorFactory]. The result of that is then placed in the cache and returned. + */ +class CachingBcPublicKeyDataDecryptorFactory( + privateKey: PGPPrivateKey, + override val subkeyIdentifier: SubkeyIdentifier +) : CustomPublicKeyDataDecryptorFactory() { + + private val decryptorFactory: BcPublicKeyDataDecryptorFactory = + BcPublicKeyDataDecryptorFactory(privateKey) + private val cachedSessions: MutableMap = mutableMapOf() + + override fun createDataDecryptor(p0: Boolean, p1: Int, p2: ByteArray?): PGPDataDecryptor { + return decryptorFactory.createDataDecryptor(p0, p1, p2) + } + + override fun createDataDecryptor(p0: AEADEncDataPacket?, p1: PGPSessionKey?): PGPDataDecryptor { + return decryptorFactory.createDataDecryptor(p0, p1) + } + + override fun createDataDecryptor( + p0: SymmetricEncIntegrityPacket?, + p1: PGPSessionKey? + ): PGPDataDecryptor { + return decryptorFactory.createDataDecryptor(p0, p1) + } + + override fun recoverSessionData( + keyAlgorithm: Int, + secKeyData: Array, + pkeskVersion: Int + ): ByteArray = + lookupSessionKeyData(secKeyData) + ?: costlyRecoverSessionData(keyAlgorithm, secKeyData, pkeskVersion).also { + cacheSessionKeyData(secKeyData, it) + } + + private fun lookupSessionKeyData(secKeyData: Array): ByteArray? = + cachedSessions[toKey(secKeyData)]?.clone() + + private fun costlyRecoverSessionData( + keyAlgorithm: Int, + secKeyData: Array, + pkeskVersion: Int + ): ByteArray = decryptorFactory.recoverSessionData(keyAlgorithm, secKeyData, pkeskVersion) + + private fun cacheSessionKeyData(secKeyData: Array, sessionKey: ByteArray) { + cachedSessions[toKey(secKeyData)] = sessionKey.clone() + } + + private fun toKey(secKeyData: Array): String = + Base64.toBase64String(secKeyData[0]) + + fun clear() { + cachedSessions.clear() + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/ConsumerOptions.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/ConsumerOptions.kt new file mode 100644 index 00000000..39a4e8e4 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/ConsumerOptions.kt @@ -0,0 +1,420 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification + +import java.io.IOException +import java.io.InputStream +import java.util.* +import org.bouncycastle.openpgp.* +import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory +import org.pgpainless.bouncycastle.extensions.getPublicKeyFor +import org.pgpainless.decryption_verification.cleartext_signatures.InMemoryMultiPassStrategy +import org.pgpainless.decryption_verification.cleartext_signatures.MultiPassStrategy +import org.pgpainless.key.SubkeyIdentifier +import org.pgpainless.key.protection.SecretKeyRingProtector +import org.pgpainless.signature.SignatureUtils +import org.pgpainless.util.Passphrase +import org.pgpainless.util.SessionKey + +/** Options for decryption and signature verification. */ +class ConsumerOptions { + + private var ignoreMDCErrors = false + var isDisableAsciiArmorCRC = false + private var forceNonOpenPgpData = false + private var verifyNotBefore: Date? = null + private var verifyNotAfter: Date? = Date() + + private val certificates = CertificateSource() + private val detachedSignatures = mutableSetOf() + private var missingCertificateCallback: MissingPublicKeyCallback? = null + + private var sessionKey: SessionKey? = null + private val customDecryptorFactories = + mutableMapOf() + private val decryptionKeys = mutableMapOf() + private val decryptionPassphrases = mutableSetOf() + private var missingKeyPassphraseStrategy = MissingKeyPassphraseStrategy.INTERACTIVE + private var multiPassStrategy: MultiPassStrategy = InMemoryMultiPassStrategy() + + /** + * Consider signatures on the message made before the given timestamp invalid. Null means no + * limitation. + * + * @param timestamp timestamp + * @return options + */ + fun verifyNotBefore(timestamp: Date?): ConsumerOptions = apply { + this.verifyNotBefore = timestamp + } + + fun getVerifyNotBefore() = verifyNotBefore + + /** + * Consider signatures on the message made after the given timestamp invalid. Null means no + * limitation. + * + * @param timestamp timestamp + * @return options + */ + fun verifyNotAfter(timestamp: Date?): ConsumerOptions = apply { + this.verifyNotAfter = timestamp + } + + fun getVerifyNotAfter() = verifyNotAfter + + /** + * Add a certificate (public key ring) for signature verification. + * + * @param verificationCert certificate for signature verification + * @return options + */ + fun addVerificationCert(verificationCert: PGPPublicKeyRing): ConsumerOptions = apply { + this.certificates.addCertificate(verificationCert) + } + + /** + * Add a set of certificates (public key rings) for signature verification. + * + * @param verificationCerts certificates for signature verification + * @return options + */ + fun addVerificationCerts(verificationCerts: PGPPublicKeyRingCollection): ConsumerOptions = + apply { + for (cert in verificationCerts) { + addVerificationCert(cert) + } + } + + /** + * Add some detached signatures from the given [InputStream] for verification. + * + * @param signatureInputStream input stream of detached signatures + * @return options + * @throws IOException in case of an IO error + * @throws PGPException in case of an OpenPGP error + */ + @Throws(IOException::class, PGPException::class) + fun addVerificationOfDetachedSignatures(signatureInputStream: InputStream): ConsumerOptions = + apply { + val signatures = SignatureUtils.readSignatures(signatureInputStream) + addVerificationOfDetachedSignatures(signatures) + } + + /** + * Add some detached signatures for verification. + * + * @param detachedSignatures detached signatures + * @return options + */ + fun addVerificationOfDetachedSignatures( + detachedSignatures: List + ): ConsumerOptions = apply { + for (signature in detachedSignatures) { + addVerificationOfDetachedSignature(signature) + } + } + + /** + * Add a detached signature for the signature verification process. + * + * @param detachedSignature detached signature + * @return options + */ + fun addVerificationOfDetachedSignature(detachedSignature: PGPSignature): ConsumerOptions = + apply { + detachedSignatures.add(detachedSignature) + } + + fun getDetachedSignatures() = detachedSignatures.toList() + + /** + * Set a callback that's used when a certificate (public key) is missing for signature + * verification. + * + * @param callback callback + * @return options + */ + fun setMissingCertificateCallback(callback: MissingPublicKeyCallback): ConsumerOptions = apply { + this.missingCertificateCallback = callback + } + + /** + * Attempt decryption using a session key. + * + * Note: PGPainless does not yet support decryption with session keys. + * + * See [RFC4880 on Session Keys](https://datatracker.ietf.org/doc/html/rfc4880#section-2.1) + * + * @param sessionKey session key + * @return options + */ + fun setSessionKey(sessionKey: SessionKey) = apply { this.sessionKey = sessionKey } + + fun getSessionKey() = sessionKey + + /** + * Add a key for message decryption. If the key is encrypted, the [SecretKeyRingProtector] is + * used to decrypt it when needed. + * + * @param key key + * @param keyRingProtector protector for the secret key + * @return options + */ + @JvmOverloads + fun addDecryptionKey( + key: PGPSecretKeyRing, + protector: SecretKeyRingProtector = SecretKeyRingProtector.unprotectedKeys() + ) = apply { decryptionKeys[key] = protector } + + /** + * Add the keys in the provided key collection for message decryption. + * + * @param keys key collection + * @param keyRingProtector protector for encrypted secret keys + * @return options + */ + @JvmOverloads + fun addDecryptionKeys( + keys: PGPSecretKeyRingCollection, + protector: SecretKeyRingProtector = SecretKeyRingProtector.unprotectedKeys() + ) = apply { + for (key in keys) { + addDecryptionKey(key, protector) + } + } + + /** + * Add a passphrase for message decryption. This passphrase will be used to try to decrypt + * messages which were symmetrically encrypted for a passphrase. + * + * See + * [Symmetrically Encrypted Data Packet](https://datatracker.ietf.org/doc/html/rfc4880#section-5.7) + * + * @param passphrase passphrase + * @return options + */ + @Deprecated( + "Deprecated in favor of addMessagePassphrase", + ReplaceWith("addMessagePassphrase(passphrase)")) + fun addDecryptionPassphrase(passphrase: Passphrase) = addMessagePassphrase(passphrase) + + /** + * Add a passphrase for message decryption. This passphrase will be used to try to decrypt + * messages which were symmetrically encrypted for a passphrase. + * + * See + * [Symmetrically Encrypted Data Packet](https://datatracker.ietf.org/doc/html/rfc4880#section-5.7) + * + * @param passphrase passphrase + * @return options + */ + fun addMessagePassphrase(passphrase: Passphrase) = apply { + decryptionPassphrases.add(passphrase) + } + + /** + * Add a custom [PublicKeyDataDecryptorFactory] which enable decryption of messages, e.g. using + * hardware-backed secret keys. (See e.g. + * [org.pgpainless.decryption_verification.HardwareSecurity.HardwareDataDecryptorFactory]). + * + * @param factory decryptor factory + * @return options + */ + fun addCustomDecryptorFactory(factory: CustomPublicKeyDataDecryptorFactory) = apply { + customDecryptorFactories[factory.subkeyIdentifier] = factory + } + + /** + * Return the custom [PublicKeyDataDecryptorFactory] that were set by the user. These factories + * can be used to decrypt session keys using a custom logic. + * + * @return custom decryptor factories + */ + fun getCustomDecryptorFactories() = customDecryptorFactories.toMap() + + /** + * Return the set of available decryption keys. + * + * @return decryption keys + */ + fun getDecryptionKeys() = decryptionKeys.keys.toSet() + + /** + * Return the set of available message decryption passphrases. + * + * @return decryption passphrases + */ + fun getDecryptionPassphrases() = decryptionPassphrases.toSet() + + /** + * Return an object holding available certificates for signature verification. + * + * @return certificate source + */ + fun getCertificateSource() = certificates + + /** + * Return the callback that gets called when a certificate for signature verification is + * missing. This method might return `null` if the users hasn't set a callback. + * + * @return missing public key callback + */ + fun getMissingCertificateCallback() = missingCertificateCallback + + /** + * Return the [SecretKeyRingProtector] for the given [PGPSecretKeyRing]. + * + * @param decryptionKeyRing secret key + * @return protector for that particular secret key + */ + fun getSecretKeyProtector(decryptionKeyRing: PGPSecretKeyRing): SecretKeyRingProtector? { + return decryptionKeys[decryptionKeyRing] + } + + /** + * By default, PGPainless will require encrypted messages to make use of SEIP data packets. + * Those are Symmetrically Encrypted Integrity Protected Data packets. Symmetrically Encrypted + * Data Packets without integrity protection are rejected by default. Furthermore, PGPainless + * will throw an exception if verification of the MDC error detection code of the SEIP packet + * fails. + * + * Failure of MDC verification indicates a tampered ciphertext, which might be the cause of an + * attack or data corruption. + * + * This method can be used to ignore MDC errors and allow PGPainless to consume encrypted data + * without integrity protection. If the flag
ignoreMDCErrors
is set to true, + * PGPainless will + * * not throw exceptions for SEIP packets with tampered ciphertext + * * not throw exceptions for SEIP packets with tampered MDC + * * not throw exceptions for MDCs with bad CTB + * * not throw exceptions for MDCs with bad length + * + * It will however still throw an exception if it encounters a SEIP packet with missing or + * truncated MDC + * + * See + * [Sym. Encrypted Integrity Protected Data Packet](https://datatracker.ietf.org/doc/html/rfc4880.section-5.13) + * + * @param ignoreMDCErrors true if MDC errors or missing MDCs shall be ignored, false otherwise. + * @return options + */ + @Deprecated("Ignoring non-integrity-protected packets is discouraged.") + fun setIgnoreMDCErrors(ignoreMDCErrors: Boolean): ConsumerOptions = apply { + this.ignoreMDCErrors = ignoreMDCErrors + } + + fun isIgnoreMDCErrors() = ignoreMDCErrors + + /** + * Force PGPainless to handle the data provided by the [InputStream] as non-OpenPGP data. This + * workaround might come in handy if PGPainless accidentally mistakes the data for binary + * OpenPGP data. + * + * @return options + */ + fun forceNonOpenPgpData(): ConsumerOptions = apply { this.forceNonOpenPgpData = true } + + /** + * Return true, if the ciphertext should be handled as binary non-OpenPGP data. + * + * @return true if non-OpenPGP data is forced + */ + fun isForceNonOpenPgpData() = forceNonOpenPgpData + + /** + * Specify the [MissingKeyPassphraseStrategy]. This strategy defines, how missing passphrases + * for unlocking secret keys are handled. In interactive mode + * ([MissingKeyPassphraseStrategy.INTERACTIVE]) PGPainless will try to obtain missing + * passphrases for secret keys via the [SecretKeyRingProtector] + * [org.pgpainless.key.protection.passphrase_provider.SecretKeyPassphraseProvider] callback. + * + * In non-interactice mode ([MissingKeyPassphraseStrategy.THROW_EXCEPTION]), PGPainless will + * instead throw a [org.pgpainless.exception.MissingPassphraseException] containing the ids of + * all keys for which there are missing passphrases. + * + * @param strategy strategy + * @return options + */ + fun setMissingKeyPassphraseStrategy(strategy: MissingKeyPassphraseStrategy): ConsumerOptions { + this.missingKeyPassphraseStrategy = strategy + return this + } + + /** + * Return the currently configured [MissingKeyPassphraseStrategy]. + * + * @return missing key passphrase strategy + */ + fun getMissingKeyPassphraseStrategy(): MissingKeyPassphraseStrategy { + return missingKeyPassphraseStrategy + } + + /** + * Set a custom multi-pass strategy for processing cleartext-signed messages. Uses + * [InMemoryMultiPassStrategy] by default. + * + * @param multiPassStrategy multi-pass caching strategy + * @return builder + */ + fun setMultiPassStrategy(multiPassStrategy: MultiPassStrategy): ConsumerOptions { + this.multiPassStrategy = multiPassStrategy + return this + } + + /** + * Return the currently configured [MultiPassStrategy]. Defaults to [InMemoryMultiPassStrategy]. + * + * @return multi-pass strategy + */ + fun getMultiPassStrategy(): MultiPassStrategy { + return multiPassStrategy + } + + /** + * Source for OpenPGP certificates. When verifying signatures on a message, this object holds + * available signer certificates. + */ + class CertificateSource { + private val explicitCertificates: MutableSet = mutableSetOf() + + /** + * Add a certificate as verification cert explicitly. + * + * @param certificate certificate + */ + fun addCertificate(certificate: PGPPublicKeyRing) { + explicitCertificates.add(certificate) + } + + /** + * Return the set of explicitly set verification certificates. + * + * @return explicitly set verification certs + */ + fun getExplicitCertificates(): Set { + return explicitCertificates.toSet() + } + + /** + * Return a certificate which contains a subkey with the given keyId. This method first + * checks all explicitly set verification certs and if no cert is found it consults the + * certificate stores. + * + * @param keyId key id + * @return certificate + */ + fun getCertificate(keyId: Long): PGPPublicKeyRing? { + return explicitCertificates.firstOrNull { it.getPublicKey(keyId) != null } + } + + fun getCertificate(signature: PGPSignature): PGPPublicKeyRing? = + explicitCertificates.firstOrNull { it.getPublicKeyFor(signature) != null } + } + + companion object { + @JvmStatic fun get() = ConsumerOptions() + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactory.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactory.kt new file mode 100644 index 00000000..cb6254dc --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactory.kt @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification + +import org.bouncycastle.openpgp.operator.AbstractPublicKeyDataDecryptorFactory +import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory +import org.pgpainless.key.SubkeyIdentifier + +/** + * Custom [PublicKeyDataDecryptorFactory] which can enable customized implementations of message + * decryption using public keys. This class can for example be used to implement message encryption + * using hardware tokens like smartcards or TPMs. + * + * @see [ConsumerOptions.addCustomDecryptorFactory] + */ +abstract class CustomPublicKeyDataDecryptorFactory : AbstractPublicKeyDataDecryptorFactory() { + + /** + * Identifier for the subkey for which this particular [CustomPublicKeyDataDecryptorFactory] is + * intended. + * + * @return subkey identifier + */ + abstract val subkeyIdentifier: SubkeyIdentifier +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/DecryptionBuilder.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/DecryptionBuilder.kt new file mode 100644 index 00000000..d1d4f8b2 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/DecryptionBuilder.kt @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification + +import java.io.InputStream + +/** + * Builder class that takes an [InputStream] of ciphertext (or plaintext signed data) and combines + * it with a configured [ConsumerOptions] object to form a [DecryptionStream] which can be used to + * decrypt an OpenPGP message or verify signatures. + */ +class DecryptionBuilder : DecryptionBuilderInterface { + + override fun onInputStream(inputStream: InputStream): DecryptionBuilderInterface.DecryptWith { + return DecryptWithImpl(inputStream) + } + + class DecryptWithImpl(val inputStream: InputStream) : DecryptionBuilderInterface.DecryptWith { + + override fun withOptions(consumerOptions: ConsumerOptions): DecryptionStream { + return OpenPgpMessageInputStream.create(inputStream, consumerOptions) + } + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/DecryptionBuilderInterface.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/DecryptionBuilderInterface.kt new file mode 100644 index 00000000..18fd4179 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/DecryptionBuilderInterface.kt @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification + +import java.io.IOException +import java.io.InputStream +import org.bouncycastle.openpgp.PGPException + +interface DecryptionBuilderInterface { + + /** + * Create a [DecryptionStream] on an [InputStream] which contains the encrypted and/or signed + * data. + * + * @param inputStream encrypted and/or signed data. + * @return api handle + */ + fun onInputStream(inputStream: InputStream): DecryptWith + + interface DecryptWith { + + /** + * Add options for decryption / signature verification, such as keys, passphrases etc. + * + * @param consumerOptions consumer options + * @return decryption stream + * @throws PGPException in case of an OpenPGP related error + * @throws IOException in case of an IO error + */ + @Throws(PGPException::class, IOException::class) + fun withOptions(consumerOptions: ConsumerOptions): DecryptionStream + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/DecryptionStream.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/DecryptionStream.kt new file mode 100644 index 00000000..86bd490a --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/DecryptionStream.kt @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification + +import java.io.InputStream + +/** + * Abstract definition of an [InputStream] which can be used to decrypt / verify OpenPGP messages. + */ +abstract class DecryptionStream : InputStream() { + + /** + * Return [MessageMetadata] about the decrypted / verified message. The [DecryptionStream] MUST + * be closed via [close] before the metadata object can be accessed. + * + * @return message metadata + */ + abstract val metadata: MessageMetadata +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/HardwareSecurity.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/HardwareSecurity.kt new file mode 100644 index 00000000..50ef3e02 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/HardwareSecurity.kt @@ -0,0 +1,95 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification + +import kotlin.jvm.Throws +import org.bouncycastle.bcpg.AEADEncDataPacket +import org.bouncycastle.bcpg.SymmetricEncIntegrityPacket +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPSessionKey +import org.bouncycastle.openpgp.operator.PGPDataDecryptor +import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory +import org.bouncycastle.openpgp.operator.bc.BcPublicKeyDataDecryptorFactory +import org.pgpainless.key.SubkeyIdentifier + +/** Enable integration of hardware-backed OpenPGP keys. */ +class HardwareSecurity { + + interface DecryptionCallback { + + /** + * Delegate decryption of a Public-Key-Encrypted-Session-Key (PKESK) to an external API for + * dealing with hardware security modules such as smartcards or TPMs. + * + * If decryption fails for some reason, a subclass of the [HardwareSecurityException] is + * thrown. + * + * @param keyId id of the key + * @param keyAlgorithm algorithm + * @param sessionKeyData encrypted session key + * @param pkeskVersion version of the Public-Key-Encrypted-Session-Key packet (3 or 6) + * @return decrypted session key + * @throws HardwareSecurityException exception + */ + @Throws(HardwareSecurityException::class) + fun decryptSessionKey( + keyId: Long, + keyAlgorithm: Int, + sessionKeyData: ByteArray, + pkeskVersion: Int + ): ByteArray + } + + /** + * Implementation of [PublicKeyDataDecryptorFactory] which delegates decryption of encrypted + * session keys to a [DecryptionCallback]. Users can provide such a callback to delegate + * decryption of messages to hardware security SDKs. + */ + class HardwareDataDecryptorFactory( + override val subkeyIdentifier: SubkeyIdentifier, + private val callback: DecryptionCallback, + ) : CustomPublicKeyDataDecryptorFactory() { + + // luckily we can instantiate the BcPublicKeyDataDecryptorFactory with null as argument. + private val factory: PublicKeyDataDecryptorFactory = BcPublicKeyDataDecryptorFactory(null) + + override fun createDataDecryptor( + withIntegrityPacket: Boolean, + encAlgorithm: Int, + key: ByteArray? + ): PGPDataDecryptor { + return factory.createDataDecryptor(withIntegrityPacket, encAlgorithm, key) + } + + override fun createDataDecryptor( + aeadEncDataPacket: AEADEncDataPacket?, + sessionKey: PGPSessionKey? + ): PGPDataDecryptor { + return factory.createDataDecryptor(aeadEncDataPacket, sessionKey) + } + + override fun createDataDecryptor( + seipd: SymmetricEncIntegrityPacket?, + sessionKey: PGPSessionKey? + ): PGPDataDecryptor { + return factory.createDataDecryptor(seipd, sessionKey) + } + + override fun recoverSessionData( + keyAlgorithm: Int, + secKeyData: Array, + pkeskVersion: Int + ): ByteArray { + return try { + callback.decryptSessionKey( + subkeyIdentifier.subkeyId, keyAlgorithm, secKeyData[0], pkeskVersion) + } catch (e: HardwareSecurityException) { + throw PGPException("Hardware-backed decryption failed.", e) + } + } + } + + class HardwareSecurityException : Exception() +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/IntegrityProtectedInputStream.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/IntegrityProtectedInputStream.kt new file mode 100644 index 00000000..4618882c --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/IntegrityProtectedInputStream.kt @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification + +import java.io.IOException +import java.io.InputStream +import org.bouncycastle.openpgp.PGPEncryptedData +import org.bouncycastle.openpgp.PGPException +import org.pgpainless.exception.ModificationDetectionException +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +class IntegrityProtectedInputStream( + private val inputStream: InputStream, + private val encryptedData: PGPEncryptedData, + private val options: ConsumerOptions +) : InputStream() { + private var closed: Boolean = false + + override fun read() = inputStream.read() + + override fun read(b: ByteArray, off: Int, len: Int) = inputStream.read(b, off, len) + + override fun close() { + if (closed) return + + closed = true + if (encryptedData.isIntegrityProtected && !options.isIgnoreMDCErrors()) { + try { + if (!encryptedData.verify()) throw ModificationDetectionException() + LOGGER.debug("Integrity Protection check passed.") + } catch (e: PGPException) { + throw IOException("Data appears to not be integrity protected.", e) + } + } + } + + companion object { + @JvmStatic + val LOGGER: Logger = LoggerFactory.getLogger(IntegrityProtectedInputStream::class.java) + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MessageInspector.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MessageInspector.kt new file mode 100644 index 00000000..acfcba51 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MessageInspector.kt @@ -0,0 +1,109 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification + +import java.io.IOException +import java.io.InputStream +import org.bouncycastle.openpgp.* +import org.pgpainless.implementation.ImplementationFactory +import org.pgpainless.util.ArmorUtils + +/** + * Inspect an OpenPGP message to determine IDs of its encryption keys or whether it is passphrase + * protected. + */ +class MessageInspector { + + /** + * Info about an OpenPGP message. + * + * @param keyIds List of recipient key ids for whom the message is encrypted. + * @param isPassphraseEncrypted true, if the message is encrypted for a passphrase + * @param isSignedOnly true, if the message is not encrypted, but signed using OnePassSignatures + */ + data class EncryptionInfo( + val keyIds: List, + val isPassphraseEncrypted: Boolean, + val isSignedOnly: Boolean + ) { + + val isEncrypted: Boolean + get() = isPassphraseEncrypted || keyIds.isNotEmpty() + } + + companion object { + + /** + * Parses parts of the provided OpenPGP message in order to determine which keys were used + * to encrypt it. + * + * @param message OpenPGP message + * @return encryption info + * @throws PGPException in case the message is broken + * @throws IOException in case of an IO error + */ + @JvmStatic + @Throws(PGPException::class, IOException::class) + fun determineEncryptionInfoForMessage(message: String): EncryptionInfo = + determineEncryptionInfoForMessage(message.byteInputStream()) + + /** + * Parses parts of the provided OpenPGP message in order to determine which keys were used + * to encrypt it. Note: This method does not rewind the passed in Stream, so you might need + * to take care of that yourselves. + * + * @param inputStream openpgp message + * @return encryption information + * @throws IOException in case of an IO error + * @throws PGPException if the message is broken + */ + @JvmStatic + @Throws(PGPException::class, IOException::class) + fun determineEncryptionInfoForMessage(inputStream: InputStream): EncryptionInfo { + return processMessage(ArmorUtils.getDecoderStream(inputStream)) + } + + @JvmStatic + @Throws(PGPException::class, IOException::class) + private fun processMessage(inputStream: InputStream): EncryptionInfo { + var objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(inputStream) + + var n: Any? + while (objectFactory.nextObject().also { n = it } != null) { + when (val next = n!!) { + is PGPOnePassSignatureList -> { + if (!next.isEmpty) { + return EncryptionInfo( + listOf(), isPassphraseEncrypted = false, isSignedOnly = true) + } + } + is PGPEncryptedDataList -> { + var isPassphraseEncrypted = false + val keyIds = mutableListOf() + for (encryptedData in next) { + if (encryptedData is PGPPublicKeyEncryptedData) { + keyIds.add(encryptedData.keyID) + } else if (encryptedData is PGPPBEEncryptedData) { + isPassphraseEncrypted = true + } + } + // Data is encrypted, we cannot go deeper + return EncryptionInfo(keyIds, isPassphraseEncrypted, false) + } + is PGPCompressedData -> { + objectFactory = + ImplementationFactory.getInstance() + .getPGPObjectFactory(PGPUtil.getDecoderStream(next.dataStream)) + continue + } + is PGPLiteralData -> { + break + } + } + } + return EncryptionInfo(listOf(), isPassphraseEncrypted = false, isSignedOnly = false) + } + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MessageMetadata.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MessageMetadata.kt new file mode 100644 index 00000000..f7238391 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MessageMetadata.kt @@ -0,0 +1,544 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification + +import java.util.* +import javax.annotation.Nonnull +import org.bouncycastle.openpgp.PGPKeyRing +import org.bouncycastle.openpgp.PGPLiteralData +import org.pgpainless.algorithm.CompressionAlgorithm +import org.pgpainless.algorithm.StreamEncoding +import org.pgpainless.algorithm.SymmetricKeyAlgorithm +import org.pgpainless.authentication.CertificateAuthority +import org.pgpainless.bouncycastle.extensions.matches +import org.pgpainless.exception.MalformedOpenPgpMessageException +import org.pgpainless.key.OpenPgpFingerprint +import org.pgpainless.key.SubkeyIdentifier +import org.pgpainless.util.SessionKey + +/** View for extracting metadata about a [Message]. */ +class MessageMetadata(val message: Message) { + + // ################################################################################################################ + // ### Encryption + // ### + // ################################################################################################################ + + /** + * The [SymmetricKeyAlgorithm] of the outermost encrypted data packet, or null if message is + * unencrypted. + */ + val encryptionAlgorithm: SymmetricKeyAlgorithm? + get() = encryptionAlgorithms.let { if (it.hasNext()) it.next() else null } + + /** + * [Iterator] of each [SymmetricKeyAlgorithm] encountered in the message. The first item + * returned by the iterator is the algorithm of the outermost encrypted data packet, the next + * item that of the next nested encrypted data packet and so on. The iterator might also be + * empty, in case of an unencrypted message. + */ + val encryptionAlgorithms: Iterator + get() = encryptionLayers.asSequence().map { it.algorithm }.iterator() + + val isEncrypted: Boolean + get() = + if (encryptionAlgorithm == null) false + else encryptionAlgorithm != SymmetricKeyAlgorithm.NULL + + fun isEncryptedFor(keys: PGPKeyRing): Boolean { + return encryptionLayers.asSequence().any { + it.recipients.any { keyId -> keys.getPublicKey(keyId) != null } + } + } + + /** + * [SessionKey] of the outermost encrypted data packet. If the message was unencrypted, this + * method returns `null`. + */ + val sessionKey: SessionKey? + get() = sessionKeys.asSequence().firstOrNull() + + /** + * [Iterator] of each [SessionKey] for all encrypted data packets in the message. The first item + * returned by the iterator is the session key of the outermost encrypted data packet, the next + * item that of the next nested encrypted data packet and so on. The iterator might also be + * empty, in case of an unencrypted message. + */ + val sessionKeys: Iterator + get() = encryptionLayers.asSequence().mapNotNull { it.sessionKey }.iterator() + + /** + * [SubkeyIdentifier] of the decryption key that was used to decrypt the outermost encryption + * layer. If the message was unencrypted or was decrypted using a passphrase, this field might + * be `null`. + */ + val decryptionKey: SubkeyIdentifier? + get() = encryptionLayers.asSequence().mapNotNull { it.decryptionKey }.firstOrNull() + + /** List containing all recipient keyIDs. */ + val recipientKeyIds: List + get() = + encryptionLayers + .asSequence() + .map { it.recipients.toMutableList() } + .reduce { all, keyIds -> + all.addAll(keyIds) + all + } + .toList() + + val encryptionLayers: Iterator + get() = + object : LayerIterator(message) { + override fun matches(layer: Packet) = layer is EncryptedData + + override fun getProperty(last: Layer) = last as EncryptedData + } + + // ################################################################################################################ + // ### Compression + // ### + // ################################################################################################################ + + /** + * [CompressionAlgorithm] of the outermost compressed data packet, or null, if the message does + * not contain any compressed data packets. + */ + val compressionAlgorithm: CompressionAlgorithm? = + compressionAlgorithms.asSequence().firstOrNull() + + /** + * [Iterator] of each [CompressionAlgorithm] encountered in the message. The first item returned + * by the iterator is the algorithm of the outermost compressed data packet, the next item that + * of the next nested compressed data packet and so on. The iterator might also be empty, in + * case of a message without any compressed data packets. + */ + val compressionAlgorithms: Iterator + get() = compressionLayers.asSequence().map { it.algorithm }.iterator() + + val compressionLayers: Iterator + get() = + object : LayerIterator(message) { + override fun matches(layer: Packet) = layer is CompressedData + + override fun getProperty(last: Layer) = last as CompressedData + } + + // ################################################################################################################ + // ### Signatures + // ### + // ################################################################################################################ + + val isUsingCleartextSignatureFramework: Boolean + get() = message.cleartextSigned + + val verifiedSignatures: List + get() = verifiedInlineSignatures.plus(verifiedDetachedSignatures) + + /** List of all rejected signatures. */ + val rejectedSignatures: List + get() = + mutableListOf() + .plus(rejectedInlineSignatures) + .plus(rejectedDetachedSignatures) + .toList() + + /** + * List of all verified inline-signatures. This list contains all acceptable, correct signatures + * that were part of the message itself. + */ + val verifiedInlineSignatures: List = + verifiedInlineSignaturesByLayer + .asSequence() + .map { it.toMutableList() } + .reduce { acc, signatureVerifications -> + acc.addAll(signatureVerifications) + acc + } + .toList() + + /** + * [Iterator] of each [List] of verified inline-signatures of the message, separated by layer. + * Since signatures might occur in different layers within a message, this method can be used to + * gain more detailed insights into what signatures were encountered at what layers of the + * message structure. Each item of the [Iterator] represents a layer of the message and contains + * only signatures from this layer. An empty list means no (or no acceptable) signatures were + * encountered in that layer. + */ + val verifiedInlineSignaturesByLayer: Iterator> + get() = + object : LayerIterator>(message) { + override fun matches(layer: Packet) = layer is Layer + + override fun getProperty(last: Layer): List { + return listOf() + .plus(last.verifiedOnePassSignatures) + .plus(last.verifiedPrependedSignatures) + } + } + + /** List of all rejected inline-signatures of the message. */ + val rejectedInlineSignatures: List = + rejectedInlineSignaturesByLayer + .asSequence() + .map { it.toMutableList() } + .reduce { acc, failures -> + acc.addAll(failures) + acc + } + .toList() + + /** + * Similar to [verifiedInlineSignaturesByLayer], this field contains all rejected + * inline-signatures of the message, but organized by layer. + */ + val rejectedInlineSignaturesByLayer: Iterator> + get() = + object : LayerIterator>(message) { + override fun matches(layer: Packet) = layer is Layer + + override fun getProperty(last: Layer): List = + mutableListOf() + .plus(last.rejectedOnePassSignatures) + .plus(last.rejectedPrependedSignatures) + } + + /** + * List of all verified detached signatures. This list contains all acceptable, correct detached + * signatures. + */ + val verifiedDetachedSignatures: List = message.verifiedDetachedSignatures + + /** List of all rejected detached signatures. */ + val rejectedDetachedSignatures: List = + message.rejectedDetachedSignatures + + /** + * True, if the message contains any (verified or rejected) signature, false if no signatures + * are present. + */ + val hasSignature: Boolean + get() = isVerifiedSigned() || hasRejectedSignatures() + + fun isVerifiedSigned(): Boolean = verifiedSignatures.isNotEmpty() + + fun hasRejectedSignatures(): Boolean = rejectedSignatures.isNotEmpty() + + /** + * Return true, if the message was signed by a certificate for which we can authenticate a + * binding to the given userId. + * + * @param userId userId + * @param email if true, treat the user-id as an email address and match all userIDs containing + * this address + * @param certificateAuthority certificate authority + * @param targetAmount targeted trust amount that needs to be reached by the binding to qualify + * as authenticated. defaults to 120. + * @return true, if we can authenticate a binding for a signing key with sufficient evidence + */ + @JvmOverloads + fun isAuthenticatablySignedBy( + userId: String, + email: Boolean, + certificateAuthority: CertificateAuthority, + targetAmount: Int = 120 + ): Boolean { + return verifiedSignatures.any { + certificateAuthority + .authenticateBinding( + it.signingKey.fingerprint, + userId, + email, + it.signature.creationTime, + targetAmount) + .authenticated + } + } + + /** + * Return rue, if the message was verifiable signed by a certificate that either has the given + * fingerprint as primary key, or as the signing subkey. + * + * @param fingerprint fingerprint + * @return true if message was signed by a cert identified by the given fingerprint + */ + fun isVerifiedSignedBy(fingerprint: OpenPgpFingerprint) = + verifiedSignatures.any { it.signingKey.matches(fingerprint) } + + fun isVerifiedSignedBy(keys: PGPKeyRing) = + verifiedSignatures.any { keys.matches(it.signingKey) } + + fun isVerifiedDetachedSignedBy(fingerprint: OpenPgpFingerprint) = + verifiedDetachedSignatures.any { it.signingKey.matches(fingerprint) } + + fun isVerifiedDetachedSignedBy(keys: PGPKeyRing) = + verifiedDetachedSignatures.any { keys.matches(it.signingKey) } + + fun isVerifiedInlineSignedBy(fingerprint: OpenPgpFingerprint) = + verifiedInlineSignatures.any { it.signingKey.matches(fingerprint) } + + fun isVerifiedInlineSignedBy(keys: PGPKeyRing) = + verifiedInlineSignatures.any { keys.matches(it.signingKey) } + + // ################################################################################################################ + // ### Literal Data + // ### + // ################################################################################################################ + + /** + * Value of the literal data packet's filename field. This value can be used to store a + * decrypted file under its original filename, but since this field is not necessarily part of + * the signed data of a message, usage of this field is discouraged. + * + * @see RFC4880 §5.9. Literal Data + * Packet + */ + val filename: String? = findLiteralData()?.fileName + + /** + * True, if the sender signals an increased degree of confidentiality by setting the filename of + * the literal data packet to a special value that indicates that the data is intended for your + * eyes only. + */ + @Deprecated("Reliance on this signaling mechanism is discouraged.") + val isForYourEyesOnly: Boolean = PGPLiteralData.CONSOLE == filename + + /** + * Value of the literal data packets modification date field. This value can be used to restore + * the modification date of a decrypted file, but since this field is not necessarily part of + * the signed data, its use is discouraged. + * + * @see RFC4880 §5.9. Literal Data + * Packet + */ + val modificationDate: Date? = findLiteralData()?.modificationDate + + /** + * Value of the format field of the literal data packet. This value indicates what format (text, + * binary data, ...) the data has. Since this field is not necessarily part of the signed data + * of a message, its usage is discouraged. + * + * @see RFC4880 §5.9. Literal Data + * Packet + */ + val literalDataEncoding: StreamEncoding? = findLiteralData()?.format + + /** + * Find the [LiteralData] layer of an OpenPGP message. This method might return null, for + * example for a cleartext signed message without OpenPGP packets. + * + * @return literal data + */ + private fun findLiteralData(): LiteralData? { + // If the message is a non-OpenPGP message with a detached signature, or a Cleartext Signed + // message, + // we might not have a Literal Data packet. + var nested = message.child ?: return null + + while (nested.hasNestedChild()) { + val layer = nested as Layer + nested = + checkNotNull(layer.child) { + // Otherwise, we MUST find a Literal Data packet, or else the message is + // malformed + "Malformed OpenPGP message. Cannot find Literal Data Packet" + } + } + return nested as LiteralData + } + + // ################################################################################################################ + // ### Message Structure + // ### + // ################################################################################################################ + + interface Packet + + interface Nested : Packet { + fun hasNestedChild(): Boolean + } + + abstract class Layer(val depth: Int) : Packet { + + init { + if (depth > MAX_LAYER_DEPTH) { + throw MalformedOpenPgpMessageException( + "Maximum packet nesting depth ($MAX_LAYER_DEPTH) exceeded.") + } + } + + val verifiedDetachedSignatures: List = mutableListOf() + val rejectedDetachedSignatures: List = mutableListOf() + val verifiedOnePassSignatures: List = mutableListOf() + val rejectedOnePassSignatures: List = mutableListOf() + val verifiedPrependedSignatures: List = mutableListOf() + val rejectedPrependedSignatures: List = mutableListOf() + + /** + * Nested child element of this layer. Might be `null`, if this layer does not have a child + * element (e.g. if this is a [LiteralData] packet). + */ + var child: Nested? = null + + fun addVerifiedDetachedSignature(signature: SignatureVerification) = apply { + (verifiedDetachedSignatures as MutableList).add(signature) + } + + fun addRejectedDetachedSignature(failure: SignatureVerification.Failure) = apply { + (rejectedDetachedSignatures as MutableList).add(failure) + } + + fun addVerifiedOnePassSignature(signature: SignatureVerification) = apply { + (verifiedOnePassSignatures as MutableList).add(signature) + } + + fun addRejectedOnePassSignature(failure: SignatureVerification.Failure) = apply { + (rejectedOnePassSignatures as MutableList).add(failure) + } + + fun addVerifiedPrependedSignature(signature: SignatureVerification) = apply { + (verifiedPrependedSignatures as MutableList).add(signature) + } + + fun addRejectedPrependedSignature(failure: SignatureVerification.Failure) = apply { + (rejectedPrependedSignatures as MutableList).add(failure) + } + + companion object { + const val MAX_LAYER_DEPTH = 16 + } + } + + /** + * Outermost OpenPGP Message structure. + * + * @param cleartextSigned whether the message is using the Cleartext Signature Framework + * @see RFC4880 §7. Cleartext + * Signature Framework + */ + class Message(var cleartextSigned: Boolean = false) : Layer(0) { + fun setCleartextSigned() = apply { cleartextSigned = true } + } + + /** + * Literal Data Packet. + * + * @param fileName value of the filename field. An empty String represents no filename. + * @param modificationDate value of the modification date field. The special value `Date(0)` + * indicates no modification date. + * @param format value of the format field. + */ + class LiteralData( + val fileName: String = "", + val modificationDate: Date = Date(0L), + val format: StreamEncoding = StreamEncoding.BINARY + ) : Nested { + + // A literal data packet MUST NOT have a child element, as its content is the plaintext + override fun hasNestedChild() = false + } + + /** + * Compressed Data Packet. + * + * @param algorithm [CompressionAlgorithm] used to compress the packet. + * @param depth nesting depth at which this packet was encountered. + */ + class CompressedData(val algorithm: CompressionAlgorithm, depth: Int) : Layer(depth), Nested { + + // A compressed data packet MUST have a child element + override fun hasNestedChild() = true + } + + /** + * Encrypted Data. + * + * @param algorithm symmetric key algorithm used to encrypt the packet. + * @param depth nesting depth at which this packet was encountered. + */ + class EncryptedData(val algorithm: SymmetricKeyAlgorithm, depth: Int) : Layer(depth), Nested { + + /** [SessionKey] used to decrypt the packet. */ + var sessionKey: SessionKey? = null + + /** List of all recipient key ids to which the packet was encrypted for. */ + val recipients: List = mutableListOf() + + fun addRecipients(keyIds: List) = apply { (recipients as MutableList).addAll(keyIds) } + + /** + * Identifier of the subkey that was used to decrypt the packet (in case of a public key + * encrypted packet). + */ + var decryptionKey: SubkeyIdentifier? = null + + // An encrypted data packet MUST have a child element + override fun hasNestedChild() = true + } + + /** + * Iterator that iterates the packet structure from outermost to innermost packet, emitting the + * results of a transformation ([getProperty]) on those packets that match ([matches]) a given + * criterion. + * + * @param message outermost structure object + */ + private abstract class LayerIterator(@Nonnull message: Message) : Iterator { + private var current: Nested? + var last: Layer? = null + var parent: Message? + + init { + parent = message + current = message.child + current?.let { + if (matches(it)) { + last = current as Layer + } + } + } + + override fun hasNext(): Boolean { + parent?.let { + if (matches(it)) { + return true + } + } + if (last == null) { + findNext() + } + return last != null + } + + override fun next(): O { + parent?.let { + if (matches(it)) { + return getProperty(it).also { parent = null } + } + } + if (last == null) { + findNext() + } + last?.let { + return getProperty(it).also { last = null } + } + throw NoSuchElementException() + } + + private fun findNext() { + while (current != null && current is Layer) { + current = (current as Layer).child + if (current != null && matches(current!!)) { + last = current as Layer + break + } + } + } + + abstract fun matches(layer: Packet): Boolean + + abstract fun getProperty(last: Layer): O + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MissingKeyPassphraseStrategy.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MissingKeyPassphraseStrategy.kt new file mode 100644 index 00000000..c5443ba8 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MissingKeyPassphraseStrategy.kt @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification + +/** Strategy defining how missing secret key passphrases are handled. */ +enum class MissingKeyPassphraseStrategy { + /** + * Try to interactively obtain key passphrases one-by-one via callbacks, eg + * [org.pgpainless.key.protection.passphrase_provider.SecretKeyPassphraseProvider]. + */ + INTERACTIVE, + + /** + * Do not try to obtain passphrases interactively and instead throw a + * [org.pgpainless.exception.MissingPassphraseException] listing all keys with missing + * passphrases. + */ + THROW_EXCEPTION +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MissingPublicKeyCallback.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MissingPublicKeyCallback.kt new file mode 100644 index 00000000..eb81847f --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MissingPublicKeyCallback.kt @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification + +import org.bouncycastle.openpgp.PGPPublicKeyRing + +fun interface MissingPublicKeyCallback { + + /** + * This method gets called if we encounter a signature made by a key which was not provided for + * signature verification. If you cannot provide the requested key, it is safe to return null + * here. PGPainless will then continue verification with the next signature. + * + * Note: The key-id might belong to a subkey, so be aware that when looking up the + * [PGPPublicKeyRing], you may not only search for the key-id on the key rings primary key! + * + * It would be super cool to provide the OpenPgp fingerprint here, but unfortunately + * one-pass-signatures only contain the key id. + * + * @param keyId ID of the missing signing (sub)key + * @return keyring containing the key or null + * @see RFC + */ + fun onMissingPublicKeyEncountered(keyId: Long): PGPPublicKeyRing? +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt new file mode 100644 index 00000000..bd24b245 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt @@ -0,0 +1,1105 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification + +import java.io.EOFException +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream +import java.util.zip.Inflater +import java.util.zip.InflaterInputStream +import openpgp.openPgpKeyId +import org.bouncycastle.bcpg.BCPGInputStream +import org.bouncycastle.bcpg.CompressionAlgorithmTags +import org.bouncycastle.bcpg.UnsupportedPacketVersionException +import org.bouncycastle.openpgp.PGPCompressedData +import org.bouncycastle.openpgp.PGPEncryptedData +import org.bouncycastle.openpgp.PGPEncryptedDataList +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPOnePassSignature +import org.bouncycastle.openpgp.PGPPBEEncryptedData +import org.bouncycastle.openpgp.PGPPrivateKey +import org.bouncycastle.openpgp.PGPPublicKey +import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData +import org.bouncycastle.openpgp.PGPPublicKeyRing +import org.bouncycastle.openpgp.PGPSecretKey +import org.bouncycastle.openpgp.PGPSecretKeyRing +import org.bouncycastle.openpgp.PGPSignature +import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory +import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory +import org.bouncycastle.util.io.TeeInputStream +import org.pgpainless.PGPainless +import org.pgpainless.algorithm.CompressionAlgorithm +import org.pgpainless.algorithm.OpenPgpPacket +import org.pgpainless.algorithm.StreamEncoding +import org.pgpainless.algorithm.SymmetricKeyAlgorithm +import org.pgpainless.bouncycastle.extensions.getPublicKeyFor +import org.pgpainless.bouncycastle.extensions.getSecretKeyFor +import org.pgpainless.bouncycastle.extensions.issuerKeyId +import org.pgpainless.bouncycastle.extensions.unlock +import org.pgpainless.decryption_verification.MessageMetadata.CompressedData +import org.pgpainless.decryption_verification.MessageMetadata.EncryptedData +import org.pgpainless.decryption_verification.MessageMetadata.Layer +import org.pgpainless.decryption_verification.MessageMetadata.LiteralData +import org.pgpainless.decryption_verification.MessageMetadata.Message +import org.pgpainless.decryption_verification.MessageMetadata.Nested +import org.pgpainless.decryption_verification.cleartext_signatures.ClearsignedMessageUtil +import org.pgpainless.decryption_verification.syntax_check.InputSymbol +import org.pgpainless.decryption_verification.syntax_check.PDA +import org.pgpainless.decryption_verification.syntax_check.StackSymbol +import org.pgpainless.exception.MalformedOpenPgpMessageException +import org.pgpainless.exception.MessageNotIntegrityProtectedException +import org.pgpainless.exception.MissingDecryptionMethodException +import org.pgpainless.exception.MissingPassphraseException +import org.pgpainless.exception.SignatureValidationException +import org.pgpainless.exception.UnacceptableAlgorithmException +import org.pgpainless.implementation.ImplementationFactory +import org.pgpainless.key.SubkeyIdentifier +import org.pgpainless.key.util.KeyRingUtils +import org.pgpainless.policy.Policy +import org.pgpainless.signature.consumer.CertificateValidator +import org.pgpainless.signature.consumer.OnePassSignatureCheck +import org.pgpainless.signature.consumer.SignatureCheck +import org.pgpainless.signature.consumer.SignatureValidator +import org.pgpainless.util.ArmoredInputStreamFactory +import org.pgpainless.util.SessionKey +import org.slf4j.LoggerFactory + +class OpenPgpMessageInputStream( + type: Type, + inputStream: InputStream, + private val options: ConsumerOptions, + private val layerMetadata: Layer, + private val policy: Policy +) : DecryptionStream() { + + private val signatures: Signatures = Signatures(options) + private var packetInputStream: TeeBCPGInputStream? = null + private var nestedInputStream: InputStream? = null + private val syntaxVerifier = PDA() + private var closed = false + + init { + + // Add detached signatures only on the outermost OpenPgpMessageInputStream + if (layerMetadata is Message) { + signatures.addDetachedSignatures(options.getDetachedSignatures()) + } + + when (type) { + Type.standard -> { + + // tee out packet bytes for signature verification + packetInputStream = + TeeBCPGInputStream(BCPGInputStream.wrap(inputStream), signatures) + + // *omnomnom* + consumePackets() + } + Type.cleartext_signed -> { + val multiPassStrategy = options.getMultiPassStrategy() + val detachedSignatures = + ClearsignedMessageUtil.detachSignaturesFromInbandClearsignedMessage( + inputStream, multiPassStrategy.messageOutputStream) + + for (signature in detachedSignatures) { + signatures.addDetachedSignature(signature) + } + + options.isForceNonOpenPgpData() + nestedInputStream = + TeeInputStream(multiPassStrategy.messageInputStream, this.signatures) + } + Type.non_openpgp -> { + packetInputStream = null + nestedInputStream = TeeInputStream(inputStream, this.signatures) + } + } + } + + enum class Type { + standard, + cleartext_signed, + non_openpgp + } + + constructor( + inputStream: InputStream, + options: ConsumerOptions, + metadata: Layer, + policy: Policy + ) : this(Type.standard, inputStream, options, metadata, policy) + + private fun consumePackets() { + val pIn = packetInputStream ?: return + + var packet: OpenPgpPacket? + + // Comsume packets, potentially stepping into nested layers + layer@ while (run { + packet = pIn.nextPacketTag() + packet + } != null) { + + signatures.nextPacket(packet!!) + // Consume packets in a layer + when (packet) { + OpenPgpPacket.LIT -> { + processLiteralData() + break@layer // nest down + } + OpenPgpPacket.COMP -> { + processCompressedData() + break@layer // nest down + } + OpenPgpPacket.OPS -> { + processOnePassSignature() // OPS is on the same layer, no nest down + } + OpenPgpPacket.SIG -> { + processSignature() // SIG is on the same layer, no nest down + } + OpenPgpPacket.PKESK, + OpenPgpPacket.SKESK, + OpenPgpPacket.SED, + OpenPgpPacket.SEIPD, + OpenPgpPacket.OED -> { + if (processEncryptedData()) { + break@layer + } + throw MissingDecryptionMethodException("No working decryption method found.") + } + OpenPgpPacket.MARKER -> { + LOGGER.debug("Skipping Marker Packet") + pIn.readMarker() + } + OpenPgpPacket.PADDING -> { + LOGGER.debug("Skipping Padding Packet") + pIn.readPacket() + } + OpenPgpPacket.SK, + OpenPgpPacket.PK, + OpenPgpPacket.SSK, + OpenPgpPacket.PSK, + OpenPgpPacket.TRUST, + OpenPgpPacket.UID, + OpenPgpPacket.UATTR -> + throw MalformedOpenPgpMessageException("Illegal Packet in Stream: $packet") + OpenPgpPacket.PADDING -> { + LOGGER.debug("Padding packet") + pIn.readPadding() + } + OpenPgpPacket.EXP_1, + OpenPgpPacket.EXP_2, + OpenPgpPacket.EXP_3, + OpenPgpPacket.EXP_4 -> + throw MalformedOpenPgpMessageException("Unsupported Packet in Stream: $packet") + else -> + throw MalformedOpenPgpMessageException("Unexpected Packet in Stream: $packet") + } + } + } + + private fun processLiteralData() { + LOGGER.debug("Literal Data Packet at depth ${layerMetadata.depth} encountered.") + syntaxVerifier.next(InputSymbol.LITERAL_DATA) + val literalData = packetInputStream!!.readLiteralData() + + // Extract Metadata + layerMetadata.child = + LiteralData( + literalData.fileName, + literalData.modificationTime, + StreamEncoding.requireFromCode(literalData.format)) + + nestedInputStream = literalData.inputStream + } + + private fun processCompressedData() { + syntaxVerifier.next(InputSymbol.COMPRESSED_DATA) + signatures.enterNesting() + val compressedData = packetInputStream!!.readCompressedData() + + // Extract Metadata + val compressionLayer = + CompressedData( + CompressionAlgorithm.requireFromId(compressedData.algorithm), + layerMetadata.depth + 1) + + LOGGER.debug( + "Compressed Data Packet (${compressionLayer.algorithm}) at depth ${layerMetadata.depth} encountered.") + nestedInputStream = + OpenPgpMessageInputStream(decompress(compressedData), options, compressionLayer, policy) + } + + private fun decompress(compressedData: PGPCompressedData): InputStream { + return when (compressedData.algorithm) { + CompressionAlgorithmTags.ZIP -> + object : InflaterInputStream(compressedData.inputStream, Inflater(true)) { + private var eof = false + + override fun fill() { + if (eof) { + throw EOFException("Unexpected end of ZIP input stream") + } + + len = `in`.read(buf, 0, buf.size) + + if (len == -1) { + buf[0] = 0 + len = 0 + eof = true + } + + inf.setInput(buf, 0, len) + } + } + CompressionAlgorithmTags.ZLIB -> + object : InflaterInputStream(compressedData.inputStream) { + private var eof = false + + override fun fill() { + if (eof) { + throw EOFException("Unexpected end of ZIP input stream") + } + + len = `in`.read(buf, 0, buf.size) + + if (len == -1) { + buf[0] = 0 + len = 0 + eof = true + } + + inf.setInput(buf, 0, len) + } + } + else -> compressedData.dataStream + } + } + + private fun processOnePassSignature() { + syntaxVerifier.next(InputSymbol.ONE_PASS_SIGNATURE) + val ops = + try { + packetInputStream!!.readOnePassSignature() + } catch (e: UnsupportedPacketVersionException) { + LOGGER.debug("Unsupported One-Pass-Signature packet version encountered.", e) + return + } + signatures.addOnePassSignature(ops) + } + + private fun processSignature() { + // true if signature corresponds to OPS + val isSigForOps = syntaxVerifier.peekStack() == StackSymbol.OPS + syntaxVerifier.next(InputSymbol.SIGNATURE) + val signature = + try { + packetInputStream!!.readSignature() + } catch (e: UnsupportedPacketVersionException) { + LOGGER.debug( + "Unsupported Signature at depth ${layerMetadata.depth} encountered.", e) + return + } + + val keyId = signature.issuerKeyId + if (isSigForOps) { + LOGGER.debug( + "Signature Packet corresponding to One-Pass-Signature by key ${keyId.openPgpKeyId()} at depth ${layerMetadata.depth} encountered.") + signatures + .leaveNesting() // TODO: Only leave nesting if all OPSs of the nesting layer are + // dealt with + signatures.addCorrespondingOnePassSignature(signature, layerMetadata, policy) + } else { + LOGGER.debug( + "Prepended Signature Packet by key ${keyId.openPgpKeyId()} at depth ${layerMetadata.depth} encountered.") + signatures.addPrependedSignature(signature) + } + } + + private fun processEncryptedData(): Boolean { + LOGGER.debug( + "Symmetrically Encrypted Data Packet at depth ${layerMetadata.depth} encountered.") + syntaxVerifier.next(InputSymbol.ENCRYPTED_DATA) + val encDataList = packetInputStream!!.readEncryptedDataList() + if (!encDataList.isIntegrityProtected && !encDataList.get(0).isAEAD) { + LOGGER.warn("Symmetrically Encrypted Data Packet is not integrity-protected.") + if (!options.isIgnoreMDCErrors()) { + throw MessageNotIntegrityProtectedException() + } + } + + val esks = SortedESKs(encDataList) + LOGGER.debug( + "Symmetrically Encrypted Integrity-Protected Data has ${esks.skesks.size} SKESK(s) and" + + " ${esks.pkesks.size + esks.anonPkesks.size} PKESK(s) from which ${esks.anonPkesks.size} PKESK(s)" + + " have an anonymous recipient.") + + // try custom decryptor factories + for ((key, decryptorFactory) in options.getCustomDecryptorFactories()) { + LOGGER.debug("Attempt decryption with custom decryptor factory with key $key.") + esks.pkesks + .filter { + // find matching PKESK + it.keyID == key.subkeyId + } + .forEach { + // attempt decryption + if (decryptPKESKAndStream(esks, key, decryptorFactory, it)) { + return true + } + } + } + + // try provided session key + if (options.getSessionKey() != null) { + val sk = options.getSessionKey()!! + LOGGER.debug("Attempt decryption with provided session key.") + throwIfUnacceptable(sk.algorithm) + + val decryptorFactory = + ImplementationFactory.getInstance().getSessionKeyDataDecryptorFactory(sk) + val layer = EncryptedData(sk.algorithm, layerMetadata.depth + 1) + val skEncData = encDataList.extractSessionKeyEncryptedData() + try { + val decrypted = skEncData.getDataStream(decryptorFactory) + layer.sessionKey = sk + val integrityProtected = + IntegrityProtectedInputStream(decrypted, skEncData, options) + nestedInputStream = + OpenPgpMessageInputStream(integrityProtected, options, layer, policy) + LOGGER.debug("Successfully decrypted data using provided session key") + return true + } catch (e: PGPException) { + // Session key mismatch? + LOGGER.debug( + "Decryption using provided session key failed. Mismatched session key and message?", + e) + } + } + + // try passwords + for (passphrase in options.getDecryptionPassphrases()) { + for (skesk in esks.skesks) { + LOGGER.debug("Attempt decryption with provided passphrase") + val algorithm = SymmetricKeyAlgorithm.requireFromId(skesk.algorithm) + if (!isAcceptable(algorithm)) { + LOGGER.debug( + "Skipping SKESK with unacceptable encapsulation algorithm $algorithm") + continue + } + + val decryptorFactory = + ImplementationFactory.getInstance().getPBEDataDecryptorFactory(passphrase) + if (decryptSKESKAndStream(esks, skesk, decryptorFactory)) { + return true + } + } + } + + val postponedDueToMissingPassphrase = + mutableListOf>() + + // try (known) secret keys + esks.pkesks.forEach { pkesk -> + LOGGER.debug("Encountered PKESK for recipient ${pkesk.keyID.openPgpKeyId()}") + val decryptionKeyCandidates = getDecryptionKeys(pkesk) + for (decryptionKeys in decryptionKeyCandidates) { + val secretKey = decryptionKeys.getSecretKeyFor(pkesk)!! + val decryptionKeyId = SubkeyIdentifier(decryptionKeys, secretKey.keyID) + if (hasUnsupportedS2KSpecifier(secretKey, decryptionKeyId)) { + continue + } + + LOGGER.debug("Attempt decryption using secret key $decryptionKeyId") + val protector = options.getSecretKeyProtector(decryptionKeys) ?: continue + if (!protector.hasPassphraseFor(secretKey.keyID)) { + LOGGER.debug( + "Missing passphrase for key $decryptionKeyId. Postponing decryption until all other keys were tried.") + postponedDueToMissingPassphrase.add(secretKey to pkesk) + continue + } + + val privateKey = secretKey.unlock(protector) + if (decryptWithPrivateKey(esks, privateKey, decryptionKeyId, pkesk)) { + return true + } + } + } + + // try anonymous secret keys + for (pkesk in esks.anonPkesks) { + for ((decryptionKeys, secretKey) in findPotentialDecryptionKeys(pkesk)) { + val decryptionKeyId = SubkeyIdentifier(decryptionKeys, secretKey.keyID) + if (hasUnsupportedS2KSpecifier(secretKey, decryptionKeyId)) { + continue + } + + LOGGER.debug("Attempt decryption of anonymous PKESK with key $decryptionKeyId.") + val protector = options.getSecretKeyProtector(decryptionKeys) ?: continue + + if (!protector.hasPassphraseFor(secretKey.keyID)) { + LOGGER.debug( + "Missing passphrase for key $decryptionKeyId. Postponing decryption until all other keys were tried.") + postponedDueToMissingPassphrase.add(secretKey to pkesk) + continue + } + + val privateKey = secretKey.unlock(protector) + if (decryptWithPrivateKey(esks, privateKey, decryptionKeyId, pkesk)) { + return true + } + } + } + + if (options.getMissingKeyPassphraseStrategy() == + MissingKeyPassphraseStrategy.THROW_EXCEPTION) { + // Non-interactive mode: Throw an exception with all locked decryption keys + postponedDueToMissingPassphrase + .map { SubkeyIdentifier(getDecryptionKey(it.first.keyID)!!, it.first.keyID) } + .also { if (it.isNotEmpty()) throw MissingPassphraseException(it.toSet()) } + } else if (options.getMissingKeyPassphraseStrategy() == + MissingKeyPassphraseStrategy.INTERACTIVE) { + for ((secretKey, pkesk) in postponedDueToMissingPassphrase) { + val keyId = secretKey.keyID + val decryptionKeys = getDecryptionKey(pkesk)!! + val decryptionKeyId = SubkeyIdentifier(decryptionKeys, keyId) + if (hasUnsupportedS2KSpecifier(secretKey, decryptionKeyId)) { + continue + } + + LOGGER.debug( + "Attempt decryption with key $decryptionKeyId while interactively requesting its passphrase.") + val protector = options.getSecretKeyProtector(decryptionKeys) ?: continue + val privateKey = secretKey.unlock(protector) + if (decryptWithPrivateKey(esks, privateKey, decryptionKeyId, pkesk)) { + return true + } + } + } else { + throw IllegalStateException("Invalid PostponedKeysStrategy set in consumer options.") + } + + // We did not yet succeed in decrypting any session key :/ + LOGGER.debug("Failed to decrypt encrypted data packet.") + return false + } + + private fun decryptWithPrivateKey( + esks: SortedESKs, + privateKey: PGPPrivateKey, + decryptionKeyId: SubkeyIdentifier, + pkesk: PGPPublicKeyEncryptedData + ): Boolean { + val decryptorFactory = + ImplementationFactory.getInstance().getPublicKeyDataDecryptorFactory(privateKey) + return decryptPKESKAndStream(esks, decryptionKeyId, decryptorFactory, pkesk) + } + + private fun hasUnsupportedS2KSpecifier( + secretKey: PGPSecretKey, + decryptionKeyId: SubkeyIdentifier + ): Boolean { + val s2k = secretKey.s2K + if (s2k != null) { + if (s2k.type in 100..110) { + LOGGER.debug( + "Skipping PKESK because key $decryptionKeyId has unsupported private S2K specifier ${s2k.type}") + return true + } + } + return false + } + + private fun decryptSKESKAndStream( + esks: SortedESKs, + skesk: PGPPBEEncryptedData, + decryptorFactory: PBEDataDecryptorFactory + ): Boolean { + try { + val decrypted = skesk.getDataStream(decryptorFactory) + val sessionKey = SessionKey(skesk.getSessionKey(decryptorFactory)) + throwIfUnacceptable(sessionKey.algorithm) + val encryptedData = EncryptedData(sessionKey.algorithm, layerMetadata.depth + 1) + encryptedData.sessionKey = sessionKey + encryptedData.addRecipients(esks.pkesks.map { it.keyID }) + LOGGER.debug("Successfully decrypted data with passphrase") + val integrityProtected = IntegrityProtectedInputStream(decrypted, skesk, options) + nestedInputStream = + OpenPgpMessageInputStream(integrityProtected, options, encryptedData, policy) + return true + } catch (e: UnacceptableAlgorithmException) { + throw e + } catch (e: PGPException) { + LOGGER.debug( + "Decryption of encrypted data packet using password failed. Password mismatch?", e) + } + return false + } + + private fun decryptPKESKAndStream( + esks: SortedESKs, + decryptionKeyId: SubkeyIdentifier, + decryptorFactory: PublicKeyDataDecryptorFactory, + pkesk: PGPPublicKeyEncryptedData + ): Boolean { + try { + val decrypted = pkesk.getDataStream(decryptorFactory) + val sessionKey = SessionKey(pkesk.getSessionKey(decryptorFactory)) + throwIfUnacceptable(sessionKey.algorithm) + + val encryptedData = + EncryptedData( + SymmetricKeyAlgorithm.requireFromId( + pkesk.getSymmetricAlgorithm(decryptorFactory)), + layerMetadata.depth + 1) + encryptedData.decryptionKey = decryptionKeyId + encryptedData.sessionKey = sessionKey + encryptedData.addRecipients(esks.pkesks.plus(esks.anonPkesks).map { it.keyID }) + LOGGER.debug("Successfully decrypted data with key $decryptionKeyId") + val integrityProtected = IntegrityProtectedInputStream(decrypted, pkesk, options) + nestedInputStream = + OpenPgpMessageInputStream(integrityProtected, options, encryptedData, policy) + return true + } catch (e: UnacceptableAlgorithmException) { + throw e + } catch (e: PGPException) { + LOGGER.debug("Decryption of encrypted data packet using secret key failed.", e) + } + return false + } + + override fun read(): Int { + if (nestedInputStream == null) { + if (packetInputStream != null) { + syntaxVerifier.next(InputSymbol.END_OF_SEQUENCE) + syntaxVerifier.assertValid() + } + return -1 + } + + val r: Int = + try { + nestedInputStream!!.read() + } catch (e: IOException) { + -1 + } + if (r != -1) { + signatures.updateLiteral(r.toByte()) + } else { + nestedInputStream!!.close() + collectMetadata() + nestedInputStream = null + if (packetInputStream != null) { + try { + consumePackets() + } catch (e: PGPException) { + throw RuntimeException(e) + } + } + signatures.finish(layerMetadata, policy) + } + return r + } + + override fun read(b: ByteArray, off: Int, len: Int): Int { + if (nestedInputStream == null) { + if (packetInputStream != null) { + syntaxVerifier.next(InputSymbol.END_OF_SEQUENCE) + syntaxVerifier.assertValid() + } + return -1 + } + val r = nestedInputStream!!.read(b, off, len) + if (r != -1) { + signatures.updateLiteral(b, off, r) + } else { + nestedInputStream!!.close() + collectMetadata() + nestedInputStream = null + if (packetInputStream != null) { + try { + consumePackets() + } catch (e: PGPException) { + throw RuntimeException(e) + } + } + signatures.finish(layerMetadata, policy) + } + return r + } + + override fun close() { + super.close() + if (closed) { + if (packetInputStream != null) { + syntaxVerifier.next(InputSymbol.END_OF_SEQUENCE) + syntaxVerifier.assertValid() + } + return + } + if (nestedInputStream != null) { + nestedInputStream!!.close() + collectMetadata() + nestedInputStream = null + } + try { + consumePackets() + } catch (e: PGPException) { + throw RuntimeException(e) + } + if (packetInputStream != null) { + syntaxVerifier.next(InputSymbol.END_OF_SEQUENCE) + syntaxVerifier.assertValid() + packetInputStream!!.close() + } + closed = true + } + + private fun collectMetadata() { + if (nestedInputStream is OpenPgpMessageInputStream) { + val child = nestedInputStream as OpenPgpMessageInputStream + layerMetadata.child = (child.layerMetadata as Nested) + } + } + + override val metadata: MessageMetadata + get() { + check(closed) { "Stream must be closed before access to metadata can be granted." } + + return MessageMetadata((layerMetadata as Message)) + } + + private fun getDecryptionKey(keyId: Long): PGPSecretKeyRing? = + options.getDecryptionKeys().firstOrNull { + it.any { k -> k.keyID == keyId } + .and(PGPainless.inspectKeyRing(it).decryptionSubkeys.any { k -> k.keyID == keyId }) + } + + private fun getDecryptionKey(pkesk: PGPPublicKeyEncryptedData): PGPSecretKeyRing? = + options.getDecryptionKeys().firstOrNull { + it.getSecretKeyFor(pkesk) != null && + PGPainless.inspectKeyRing(it).decryptionSubkeys.any { subkey -> + when (pkesk.version) { + 3 -> pkesk.keyID == subkey.keyID + else -> throw NotImplementedError("Version 6 PKESK not yet supported.") + } + } + } + + private fun getDecryptionKeys(pkesk: PGPPublicKeyEncryptedData): List = + options.getDecryptionKeys().filter { + it.getSecretKeyFor(pkesk) != null && + PGPainless.inspectKeyRing(it).decryptionSubkeys.any { subkey -> + when (pkesk.version) { + 3 -> pkesk.keyID == subkey.keyID + else -> throw NotImplementedError("Version 6 PKESK not yet supported.") + } + } + } + + private fun findPotentialDecryptionKeys( + pkesk: PGPPublicKeyEncryptedData + ): List> { + val algorithm = pkesk.algorithm + val candidates = mutableListOf>() + options.getDecryptionKeys().forEach { + val info = PGPainless.inspectKeyRing(it) + for (key in info.decryptionSubkeys) { + if (key.algorithm == algorithm && info.isSecretKeyAvailable(key.keyID)) { + candidates.add(it to it.getSecretKey(key.keyID)) + } + } + } + return candidates + } + + private fun isAcceptable(algorithm: SymmetricKeyAlgorithm): Boolean = + policy.symmetricKeyDecryptionAlgorithmPolicy.isAcceptable(algorithm) + + private fun throwIfUnacceptable(algorithm: SymmetricKeyAlgorithm) { + if (!isAcceptable(algorithm)) { + throw UnacceptableAlgorithmException( + "Symmetric-Key algorithm $algorithm is not acceptable for message decryption.") + } + } + + private class SortedESKs(esks: PGPEncryptedDataList) { + val skesks: List + val pkesks: List + val anonPkesks: List + + init { + skesks = mutableListOf() + pkesks = mutableListOf() + anonPkesks = mutableListOf() + for (esk in esks) { + if (esk is PGPPBEEncryptedData) { + skesks.add(esk) + } else if (esk is PGPPublicKeyEncryptedData) { + if (esk.keyID != 0L) { + pkesks.add(esk) + } else { + anonPkesks.add(esk) + } + } else { + throw IllegalArgumentException("Unknown ESK class type ${esk.javaClass}") + } + } + } + + val all: List + get() = skesks.plus(pkesks).plus(anonPkesks) + } + + private class Signatures(val options: ConsumerOptions) : OutputStream() { + val detachedSignatures = mutableListOf() + val prependedSignatures = mutableListOf() + val onePassSignatures = mutableListOf() + val opsUpdateStack = ArrayDeque>() + var literalOPS = mutableListOf() + val correspondingSignatures = mutableListOf() + val prependedSignaturesWithMissingCert = mutableListOf() + val inbandSignaturesWithMissingCert = mutableListOf() + val detachedSignaturesWithMissingCert = mutableListOf() + var isLiteral = true + + fun addDetachedSignatures(signatures: Collection) { + for (signature in signatures) { + addDetachedSignature(signature) + } + } + + fun addDetachedSignature(signature: PGPSignature) { + val check = initializeSignature(signature) + val keyId = signature.issuerKeyId + if (check != null) { + detachedSignatures.add(check) + } else { + LOGGER.debug( + "No suitable certificate for verification of signature by key ${keyId.openPgpKeyId()} found.") + detachedSignaturesWithMissingCert.add( + SignatureVerification.Failure( + signature, null, SignatureValidationException("Missing verification key."))) + } + } + + fun addPrependedSignature(signature: PGPSignature) { + val check = initializeSignature(signature) + val keyId = signature.issuerKeyId + if (check != null) { + prependedSignatures.add(check) + } else { + LOGGER.debug( + "No suitable certificate for verification of signature by key ${keyId.openPgpKeyId()} found.") + prependedSignaturesWithMissingCert.add( + SignatureVerification.Failure( + signature, null, SignatureValidationException("Missing verification key"))) + } + } + + fun initializeSignature(signature: PGPSignature): SignatureCheck? { + val certificate = findCertificate(signature) ?: return null + val publicKey = certificate.getPublicKeyFor(signature) ?: return null + val verifierKey = SubkeyIdentifier(certificate, publicKey.keyID) + initialize(signature, publicKey) + return SignatureCheck(signature, certificate, verifierKey) + } + + fun addOnePassSignature(signature: PGPOnePassSignature) { + val certificate = findCertificate(signature) + + if (certificate != null) { + val publicKey = certificate.getPublicKeyFor(signature) + if (publicKey != null) { + val ops = OnePassSignatureCheck(signature, certificate) + initialize(signature, publicKey) + onePassSignatures.add(ops) + literalOPS.add(ops) + } + } + if (signature.isContaining) { + enterNesting() + } + } + + fun addCorrespondingOnePassSignature( + signature: PGPSignature, + layer: Layer, + policy: Policy + ) { + var found = false + val keyId = signature.issuerKeyId + for ((i, check) in onePassSignatures.withIndex().reversed()) { + if (check.onePassSignature.keyID != keyId) { + continue + } + found = true + + if (check.signature != null) { + continue + } + + check.signature = signature + val verification = + SignatureVerification( + signature, + SubkeyIdentifier(check.verificationKeys, check.onePassSignature.keyID)) + + try { + SignatureValidator.signatureWasCreatedInBounds( + options.getVerifyNotBefore(), options.getVerifyNotAfter()) + .verify(signature) + CertificateValidator.validateCertificateAndVerifyOnePassSignature(check, policy) + LOGGER.debug("Acceptable signature by key ${verification.signingKey}") + layer.addVerifiedOnePassSignature(verification) + } catch (e: SignatureValidationException) { + LOGGER.debug("Rejected signature by key ${verification.signingKey}", e) + layer.addRejectedOnePassSignature( + SignatureVerification.Failure(verification, e)) + } + break + } + + if (!found) { + LOGGER.debug( + "No suitable certificate for verification of signature by key ${keyId.openPgpKeyId()} found.") + inbandSignaturesWithMissingCert.add( + SignatureVerification.Failure( + signature, null, SignatureValidationException("Missing verification key."))) + } + } + + fun enterNesting() { + opsUpdateStack.addFirst(literalOPS) + literalOPS = mutableListOf() + } + + fun leaveNesting() { + if (opsUpdateStack.isEmpty()) { + return + } + opsUpdateStack.removeFirst() + } + + private fun findCertificate(signature: PGPSignature): PGPPublicKeyRing? { + val cert = options.getCertificateSource().getCertificate(signature) + if (cert != null) { + return cert + } + + if (options.getMissingCertificateCallback() != null) { + return options + .getMissingCertificateCallback()!! + .onMissingPublicKeyEncountered(signature.keyID) + } + return null // TODO: Missing cert for sig + } + + private fun findCertificate(signature: PGPOnePassSignature): PGPPublicKeyRing? { + val cert = options.getCertificateSource().getCertificate(signature.keyID) + if (cert != null) { + return cert + } + + if (options.getMissingCertificateCallback() != null) { + return options + .getMissingCertificateCallback()!! + .onMissingPublicKeyEncountered(signature.keyID) + } + return null // TODO: Missing cert for sig + } + + fun updateLiteral(b: Byte) { + for (ops in literalOPS) { + ops.onePassSignature.update(b) + } + + for (detached in detachedSignatures) { + detached.signature.update(b) + } + + for (prepended in prependedSignatures) { + prepended.signature.update(b) + } + } + + fun updateLiteral(buf: ByteArray, off: Int, len: Int) { + for (ops in literalOPS) { + ops.onePassSignature.update(buf, off, len) + } + + for (detached in detachedSignatures) { + detached.signature.update(buf, off, len) + } + + for (prepended in prependedSignatures) { + prepended.signature.update(buf, off, len) + } + } + + fun updatePacket(b: Byte) { + for (nestedOPSs in opsUpdateStack.reversed()) { + for (ops in nestedOPSs) { + ops.onePassSignature.update(b) + } + } + } + + fun updatePacket(buf: ByteArray, off: Int, len: Int) { + for (nestedOPSs in opsUpdateStack.reversed()) { + for (ops in nestedOPSs) { + ops.onePassSignature.update(buf, off, len) + } + } + } + + fun finish(layer: Layer, policy: Policy) { + for (detached in detachedSignatures) { + val verification = + SignatureVerification(detached.signature, detached.signingKeyIdentifier) + try { + SignatureValidator.signatureWasCreatedInBounds( + options.getVerifyNotBefore(), options.getVerifyNotAfter()) + .verify(detached.signature) + CertificateValidator.validateCertificateAndVerifyInitializedSignature( + detached.signature, + KeyRingUtils.publicKeys(detached.signingKeyRing), + policy) + LOGGER.debug("Acceptable signature by key ${verification.signingKey}") + layer.addVerifiedDetachedSignature(verification) + } catch (e: SignatureValidationException) { + LOGGER.debug("Rejected signature by key ${verification.signingKey}", e) + layer.addRejectedDetachedSignature( + SignatureVerification.Failure(verification, e)) + } + } + + for (prepended in prependedSignatures) { + val verification = + SignatureVerification(prepended.signature, prepended.signingKeyIdentifier) + try { + SignatureValidator.signatureWasCreatedInBounds( + options.getVerifyNotBefore(), options.getVerifyNotAfter()) + .verify(prepended.signature) + CertificateValidator.validateCertificateAndVerifyInitializedSignature( + prepended.signature, + KeyRingUtils.publicKeys(prepended.signingKeyRing), + policy) + LOGGER.debug("Acceptable signature by key ${verification.signingKey}") + layer.addVerifiedPrependedSignature(verification) + } catch (e: SignatureValidationException) { + LOGGER.debug("Rejected signature by key ${verification.signingKey}", e) + layer.addRejectedPrependedSignature( + SignatureVerification.Failure(verification, e)) + } + } + + for (rejected in inbandSignaturesWithMissingCert) { + layer.addRejectedOnePassSignature(rejected) + } + + for (rejected in prependedSignaturesWithMissingCert) { + layer.addRejectedPrependedSignature(rejected) + } + + for (rejected in detachedSignaturesWithMissingCert) { + layer.addRejectedDetachedSignature(rejected) + } + } + + override fun write(b: Int) { + updatePacket(b.toByte()) + } + + override fun write(buf: ByteArray, off: Int, len: Int) { + updatePacket(buf, off, len) + } + + fun nextPacket(nextPacket: OpenPgpPacket) { + if (nextPacket == OpenPgpPacket.LIT) { + isLiteral = true + if (literalOPS.isEmpty() && opsUpdateStack.isNotEmpty()) { + literalOPS = opsUpdateStack.removeFirst() + } + } else { + isLiteral = false + } + } + + companion object { + @JvmStatic + private fun initialize(signature: PGPSignature, publicKey: PGPPublicKey) { + val verifierProvider = + ImplementationFactory.getInstance().pgpContentVerifierBuilderProvider + try { + signature.init(verifierProvider, publicKey) + } catch (e: PGPException) { + throw RuntimeException(e) + } + } + + @JvmStatic + private fun initialize(ops: PGPOnePassSignature, publicKey: PGPPublicKey) { + val verifierProvider = + ImplementationFactory.getInstance().pgpContentVerifierBuilderProvider + try { + ops.init(verifierProvider, publicKey) + } catch (e: PGPException) { + throw RuntimeException(e) + } + } + } + } + + companion object { + @JvmStatic + private val LOGGER = LoggerFactory.getLogger(OpenPgpMessageInputStream::class.java) + + @JvmStatic + fun create(inputStream: InputStream, options: ConsumerOptions) = + create(inputStream, options, PGPainless.getPolicy()) + + @JvmStatic + fun create(inputStream: InputStream, options: ConsumerOptions, policy: Policy) = + create(inputStream, options, Message(), policy) + + @JvmStatic + internal fun create( + inputStream: InputStream, + options: ConsumerOptions, + metadata: Layer, + policy: Policy + ): OpenPgpMessageInputStream { + val openPgpIn = OpenPgpInputStream(inputStream) + openPgpIn.reset() + + if (openPgpIn.isNonOpenPgp || options.isForceNonOpenPgpData()) { + return OpenPgpMessageInputStream( + Type.non_openpgp, openPgpIn, options, metadata, policy) + } + + if (openPgpIn.isBinaryOpenPgp) { + // Simply consume OpenPGP message + return OpenPgpMessageInputStream( + Type.standard, openPgpIn, options, metadata, policy) + } + + return if (openPgpIn.isAsciiArmored) { + val armorIn = ArmoredInputStreamFactory.get(openPgpIn) + if (armorIn.isClearText) { + (metadata as Message).setCleartextSigned() + OpenPgpMessageInputStream( + Type.cleartext_signed, armorIn, options, metadata, policy) + } else { + // Simply consume dearmored OpenPGP message + OpenPgpMessageInputStream(Type.standard, armorIn, options, metadata, policy) + } + } else { + throw AssertionError("Cannot deduce type of data.") + } + } + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/SignatureVerification.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/SignatureVerification.kt new file mode 100644 index 00000000..3e00fbb2 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/SignatureVerification.kt @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification + +import org.bouncycastle.openpgp.PGPSignature +import org.pgpainless.decryption_verification.SignatureVerification.Failure +import org.pgpainless.exception.SignatureValidationException +import org.pgpainless.key.SubkeyIdentifier +import org.pgpainless.signature.SignatureUtils + +/** + * Tuple of a signature and an identifier of its corresponding verification key. Semantic meaning of + * the signature verification (success, failure) is merely given by context. E.g. + * [MessageMetadata.getVerifiedInlineSignatures] contains verified verifications, while the class + * [Failure] contains failed verifications. + * + * @param signature PGPSignature object + * @param signingKey [SubkeyIdentifier] of the (sub-) key that is used for signature verification. + * Note, that this might be null, e.g. in case of a [Failure] due to missing verification key. + */ +data class SignatureVerification(val signature: PGPSignature, val signingKey: SubkeyIdentifier) { + + override fun toString(): String { + return "Signature: ${SignatureUtils.getSignatureDigestPrefix(signature)};" + + " Key: $signingKey;" + } + + /** + * Tuple object of a [SignatureVerification] and the corresponding + * [SignatureValidationException] that caused the verification to fail. + * + * @param signatureVerification verification (tuple of [PGPSignature] and corresponding + * [SubkeyIdentifier]) + * @param validationException exception that caused the verification to fail + */ + data class Failure( + val signature: PGPSignature, + val signingKey: SubkeyIdentifier?, + val validationException: SignatureValidationException + ) { + + constructor( + verification: SignatureVerification, + validationException: SignatureValidationException + ) : this(verification.signature, verification.signingKey, validationException) + + override fun toString(): String { + return "Signature: ${SignatureUtils.getSignatureDigestPrefix(signature)}; Key: ${signingKey?.toString() ?: "null"}; Failure: ${validationException.message}" + } + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/TeeBCPGInputStream.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/TeeBCPGInputStream.kt new file mode 100644 index 00000000..a9b353ca --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/TeeBCPGInputStream.kt @@ -0,0 +1,138 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification + +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream +import org.bouncycastle.bcpg.BCPGInputStream +import org.bouncycastle.bcpg.MarkerPacket +import org.bouncycastle.bcpg.Packet +import org.bouncycastle.openpgp.PGPCompressedData +import org.bouncycastle.openpgp.PGPEncryptedDataList +import org.bouncycastle.openpgp.PGPLiteralData +import org.bouncycastle.openpgp.PGPOnePassSignature +import org.bouncycastle.openpgp.PGPPadding +import org.bouncycastle.openpgp.PGPSignature +import org.pgpainless.algorithm.OpenPgpPacket + +/** + * Since we need to update signatures with data from the underlying stream, this class is used to + * tee out the data. Unfortunately we cannot simply override [BCPGInputStream.read] to tee the data + * out though, since [BCPGInputStream.readPacket] inconsistently calls a mix of + * [BCPGInputStream.read] and [InputStream.read] of the underlying stream. This would cause the + * second length byte to get swallowed up. + * + * Therefore, this class delegates the teeing to an [DelayedTeeInputStream] which wraps the + * underlying stream. Since calling [BCPGInputStream.nextPacketTag] reads up to and including the + * next packets tag, we need to delay teeing out that byte to signature verifiers. Hence, the + * reading methods of the [TeeBCPGInputStream] handle pushing this byte to the output stream using + * [DelayedTeeInputStream.squeeze]. + */ +class TeeBCPGInputStream(inputStream: BCPGInputStream, outputStream: OutputStream) { + + private val delayedTee: DelayedTeeInputStream + private val packetInputStream: BCPGInputStream + + init { + delayedTee = DelayedTeeInputStream(inputStream, outputStream) + packetInputStream = BCPGInputStream(delayedTee) + } + + fun nextPacketTag(): OpenPgpPacket? { + return packetInputStream.nextPacketTag().let { + if (it == -1) null else OpenPgpPacket.requireFromTag(it) + } + } + + fun readPacket(): Packet = packetInputStream.readPacket() + + fun readCompressedData(): PGPCompressedData { + delayedTee.squeeze() + return PGPCompressedData(packetInputStream) + } + + fun readLiteralData(): PGPLiteralData { + delayedTee.squeeze() + return PGPLiteralData(packetInputStream) + } + + fun readEncryptedDataList(): PGPEncryptedDataList { + delayedTee.squeeze() + return PGPEncryptedDataList(packetInputStream) + } + + fun readOnePassSignature(): PGPOnePassSignature { + return PGPOnePassSignature(packetInputStream).also { delayedTee.squeeze() } + } + + fun readSignature(): PGPSignature { + return PGPSignature(packetInputStream).also { delayedTee.squeeze() } + } + + fun readMarker(): MarkerPacket { + return (readPacket() as MarkerPacket).also { delayedTee.squeeze() } + } + + fun readPadding(): PGPPadding { + return PGPPadding(packetInputStream).also { delayedTee.squeeze() } + } + + fun close() { + packetInputStream.close() + } + + class DelayedTeeInputStream( + private val inputStream: InputStream, + private val outputStream: OutputStream + ) : InputStream() { + private var last: Int = -1 + + override fun read(): Int { + if (last != -1) { + outputStream.write(last) + } + return try { + last = inputStream.read() + last + } catch (e: IOException) { + if (e.message?.contains("crc check failed in armored message") == true) { + throw e + } + -1 + } + } + + override fun read(b: ByteArray, off: Int, len: Int): Int { + if (last != -1) { + outputStream.write(last) + } + + inputStream.read(b, off, len).let { r -> + last = + if (r > 0) { + outputStream.write(b, off, r - 1) + b[off + r - 1].toInt() + } else { + -1 + } + return r + } + } + + /** Squeeze the last byte out and update the output stream. */ + fun squeeze() { + if (last != -1) { + outputStream.write(last) + } + last = -1 + } + + override fun close() { + inputStream.close() + outputStream.close() + } + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/cleartext_signatures/ClearsignedMessageUtil.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/cleartext_signatures/ClearsignedMessageUtil.kt new file mode 100644 index 00000000..78614a96 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/cleartext_signatures/ClearsignedMessageUtil.kt @@ -0,0 +1,159 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification.cleartext_signatures + +import java.io.* +import kotlin.jvm.Throws +import org.bouncycastle.bcpg.ArmoredInputStream +import org.bouncycastle.openpgp.PGPSignatureList +import org.bouncycastle.util.Strings +import org.pgpainless.exception.WrongConsumingMethodException +import org.pgpainless.implementation.ImplementationFactory +import org.pgpainless.util.ArmoredInputStreamFactory + +/** + * Utility class to deal with cleartext-signed messages. Based on Bouncycastle's + * [org.bouncycastle.openpgp.examples.ClearSignedFileProcessor]. + */ +class ClearsignedMessageUtil { + + companion object { + + /** + * Dearmor a clearsigned message, detach the inband signatures and write the plaintext + * message to the provided messageOutputStream. + * + * @param clearsignedInputStream input stream containing a clearsigned message + * @param messageOutputStream output stream to which the dearmored message shall be written + * @return signatures + * @throws IOException if the message is not clearsigned or some other IO error happens + * @throws WrongConsumingMethodException in case the armored message is not cleartext signed + */ + @JvmStatic + @Throws(WrongConsumingMethodException::class, IOException::class) + fun detachSignaturesFromInbandClearsignedMessage( + clearsignedInputStream: InputStream, + messageOutputStream: OutputStream + ): PGPSignatureList { + val input: ArmoredInputStream = + if (clearsignedInputStream is ArmoredInputStream) { + clearsignedInputStream + } else { + ArmoredInputStreamFactory.get(clearsignedInputStream) + } + + if (!input.isClearText) { + throw WrongConsumingMethodException( + "Message isn't using the Cleartext Signature Framework.") + } + + BufferedOutputStream(messageOutputStream).use { output -> + val lineOut = ByteArrayOutputStream() + var lookAhead = readInputLine(lineOut, input) + val lineSep = getLineSeparator() + + if (lookAhead != -1 && input.isClearText) { + var line = lineOut.toByteArray() + output.write(line, 0, getLengthWithoutSeparatorOrTrailingWhitespace(line)) + + while (lookAhead != -1 && input.isClearText) { + lookAhead = readInputLine(lineOut, lookAhead, input) + line = lineOut.toByteArray() + output.write(lineSep) + output.write(line, 0, getLengthWithoutSeparatorOrTrailingWhitespace(line)) + } + } else { + if (lookAhead != -1) { + val line = lineOut.toByteArray() + output.write(line, 0, getLengthWithoutSeparatorOrTrailingWhitespace(line)) + } + } + } + + val objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(input) + val next = objectFactory.nextObject() ?: PGPSignatureList(arrayOf()) + return next as PGPSignatureList + } + + @JvmStatic + private fun readInputLine(bOut: ByteArrayOutputStream, fIn: InputStream): Int { + bOut.reset() + + var lookAhead = -1 + var ch: Int + + while (fIn.read().also { ch = it } >= 0) { + bOut.write(ch) + if (ch == '\r'.code || ch == '\n'.code) { + lookAhead = readPassedEOL(bOut, ch, fIn) + break + } + } + + return lookAhead + } + + @JvmStatic + private fun readInputLine( + bOut: ByteArrayOutputStream, + lookAhead: Int, + fIn: InputStream + ): Int { + var mLookAhead = lookAhead + bOut.reset() + var ch = mLookAhead + do { + bOut.write(ch) + if (ch == '\r'.code || ch == '\n'.code) { + mLookAhead = readPassedEOL(bOut, ch, fIn) + break + } + } while (fIn.read().also { ch = it } >= 0) + if (ch < 0) { + mLookAhead = -1 + } + return mLookAhead + } + + @JvmStatic + private fun readPassedEOL(bOut: ByteArrayOutputStream, lastCh: Int, fIn: InputStream): Int { + var lookAhead = fIn.read() + if (lastCh == '\r'.code && lookAhead == '\n'.code) { + bOut.write(lookAhead) + lookAhead = fIn.read() + } + return lookAhead + } + + @JvmStatic + private fun getLineSeparator(): ByteArray { + val nl = Strings.lineSeparator() + val nlBytes = ByteArray(nl.length) + for (i in nlBytes.indices) { + nlBytes[i] = nl[i].code.toByte() + } + return nlBytes + } + + @JvmStatic + private fun getLengthWithoutSeparatorOrTrailingWhitespace(line: ByteArray): Int { + var end = line.size - 1 + while (end >= 0 && isWhiteSpace(line[end])) { + end-- + } + return end + 1 + } + + @JvmStatic + private fun isLineEnding(b: Byte): Boolean { + return b == '\r'.code.toByte() || b == '\n'.code.toByte() + } + + @JvmStatic + private fun isWhiteSpace(b: Byte): Boolean { + return isLineEnding(b) || b == '\t'.code.toByte() || b == ' '.code.toByte() + } + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/cleartext_signatures/InMemoryMultiPassStrategy.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/cleartext_signatures/InMemoryMultiPassStrategy.kt new file mode 100644 index 00000000..da7c7cec --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/cleartext_signatures/InMemoryMultiPassStrategy.kt @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification.cleartext_signatures + +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream + +/** + * Implementation of the [MultiPassStrategy]. This class keeps the read data in memory by caching + * the data inside a [ByteArrayOutputStream]. + * + * Note, that this class is suitable and efficient for processing small amounts of data. For larger + * data like encrypted files, use of the [WriteToFileMultiPassStrategy] is recommended to prevent + * [OutOfMemoryError] and other issues. + */ +class InMemoryMultiPassStrategy : MultiPassStrategy { + + private val cache = ByteArrayOutputStream() + + override val messageOutputStream: ByteArrayOutputStream + get() = cache + + override val messageInputStream: ByteArrayInputStream + get() = ByteArrayInputStream(getBytes()) + + fun getBytes(): ByteArray = messageOutputStream.toByteArray() +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/cleartext_signatures/MultiPassStrategy.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/cleartext_signatures/MultiPassStrategy.kt new file mode 100644 index 00000000..4ef7ef3a --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/cleartext_signatures/MultiPassStrategy.kt @@ -0,0 +1,72 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification.cleartext_signatures + +import java.io.* + +/** + * Since for verification of cleartext signed messages, we need to read the whole data twice in + * order to verify signatures, a strategy for how to cache the read data is required. Otherwise, + * large data kept in memory could cause an [OutOfMemoryError] or other issues. + * + * This is an Interface that describes a strategy to deal with the fact that detached signatures + * require multiple passes to do verification. + * + * This interface can be used to write the signed data stream out via [messageOutputStream] and + * later get access to the data again via [messageInputStream]. Thereby the detail where the data is + * being stored (memory, file, etc.) can be abstracted away. + */ +interface MultiPassStrategy { + + /** + * Provide an [OutputStream] into which the signed data can be read into. + * + * @return output stream + * @throws IOException io error + */ + val messageOutputStream: OutputStream + + /** + * Provide an [InputStream] which contains the data that was previously written away in + * [messageOutputStream]. + * + * As there may be multiple signatures that need to be processed, each call of this method MUST + * return a new [InputStream]. + * + * @return input stream + * @throws IOException io error + */ + val messageInputStream: InputStream + + companion object { + + /** + * Write the message content out to a file and re-read it to verify signatures. This + * strategy is best suited for larger messages (e.g. plaintext signed files) which might not + * fit into memory. After the message has been processed completely, the messages content + * are available at the provided file. + * + * @param file target file + * @return strategy + */ + @JvmStatic + fun writeMessageToFile(file: File): MultiPassStrategy { + return WriteToFileMultiPassStrategy(file) + } + + /** + * Read the message content into memory. This strategy is best suited for small messages + * which fit into memory. After the message has been processed completely, the message + * content can be accessed by calling [ByteArrayOutputStream.toByteArray] on + * [messageOutputStream]. + * + * @return strategy + */ + @JvmStatic + fun keepMessageInMemory(): InMemoryMultiPassStrategy { + return InMemoryMultiPassStrategy() + } + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/cleartext_signatures/WriteToFileMultiPassStrategy.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/cleartext_signatures/WriteToFileMultiPassStrategy.kt new file mode 100644 index 00000000..88fcf6f1 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/cleartext_signatures/WriteToFileMultiPassStrategy.kt @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification.cleartext_signatures + +import java.io.* + +/** + * Implementation of the [MultiPassStrategy]. When processing signed data the first time, the data + * is being written out into a file. For the second pass, that file is being read again. + * + * This strategy is recommended when larger amounts of data need to be processed. For smaller files, + * [InMemoryMultiPassStrategy] yields higher efficiency. + * + * @param file file to write the data to and read from + */ +class WriteToFileMultiPassStrategy(private val file: File) : MultiPassStrategy { + + override val messageOutputStream: OutputStream + @Throws(IOException::class) + get() { + if (!file.exists()) { + if (!file.createNewFile()) { + throw IOException("New file '${file.absolutePath}' could not be created.") + } + } + return FileOutputStream(file) + } + + override val messageInputStream: InputStream + @Throws(IOException::class) + get() { + if (!file.exists()) { + throw IOException("File '${file.absolutePath}' does no longer exist.") + } + return FileInputStream(file) + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/InputSymbol.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/InputSymbol.kt new file mode 100644 index 00000000..2bea3356 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/InputSymbol.kt @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification.syntax_check + +enum class InputSymbol { + /** A [PGPLiteralData] packet. */ + LITERAL_DATA, + /** A [PGPSignatureList] object. */ + SIGNATURE, + /** A [PGPOnePassSignatureList] object. */ + ONE_PASS_SIGNATURE, + /** + * A [PGPCompressedData] packet. The contents of this packet MUST form a valid OpenPGP message, + * so a nested PDA is opened to verify its nested packet sequence. + */ + COMPRESSED_DATA, + /** + * A [PGPEncryptedDataList] object. This object combines multiple ESKs and the corresponding + * Symmetrically Encrypted (possibly Integrity Protected) Data packet. + */ + ENCRYPTED_DATA, + /** + * Marks the end of a (sub-) sequence. This input is given if the end of an OpenPGP message is + * reached. This might be the case for the end of the whole ciphertext, or the end of a packet + * with nested contents (e.g. the end of a Compressed Data packet). + */ + END_OF_SEQUENCE +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.kt new file mode 100644 index 00000000..127d10f8 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.kt @@ -0,0 +1,91 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification.syntax_check + +import org.pgpainless.exception.MalformedOpenPgpMessageException + +/** + * This class describes the syntax for OpenPGP messages as specified by rfc4880. + * + * See [rfc4880 - §11.3. OpenPGP Messages](https://www.rfc-editor.org/rfc/rfc4880#section-11.3) See + * [Blog post about theoretic background and translation of grammar to PDA syntax](https://blog.jabberhead.tk/2022/09/14/using-pushdown-automata-to-verify-packet-sequences/) + * See + * [Blog post about practically implementing the PDA for packet syntax validation](https://blog.jabberhead.tk/2022/10/26/implementing-packet-sequence-validation-using-pushdown-automata/) + */ +class OpenPgpMessageSyntax : Syntax { + + override fun transition(from: State, input: InputSymbol, stackItem: StackSymbol?): Transition { + return when (from) { + State.OPENPGP_MESSAGE -> fromOpenPgpMessage(input, stackItem) + State.LITERAL_MESSAGE -> fromLiteralMessage(input, stackItem) + State.COMPRESSED_MESSAGE -> fromCompressedMessage(input, stackItem) + State.ENCRYPTED_MESSAGE -> fromEncryptedMessage(input, stackItem) + State.VALID -> fromValid(input, stackItem) + else -> throw MalformedOpenPgpMessageException(from, input, stackItem) + } + } + + fun fromOpenPgpMessage(input: InputSymbol, stackItem: StackSymbol?): Transition { + if (stackItem !== StackSymbol.MSG) { + throw MalformedOpenPgpMessageException(State.OPENPGP_MESSAGE, input, stackItem) + } + return when (input) { + InputSymbol.LITERAL_DATA -> Transition(State.LITERAL_MESSAGE) + InputSymbol.SIGNATURE -> Transition(State.OPENPGP_MESSAGE, StackSymbol.MSG) + InputSymbol.ONE_PASS_SIGNATURE -> + Transition(State.OPENPGP_MESSAGE, StackSymbol.OPS, StackSymbol.MSG) + InputSymbol.COMPRESSED_DATA -> Transition(State.COMPRESSED_MESSAGE) + InputSymbol.ENCRYPTED_DATA -> Transition(State.ENCRYPTED_MESSAGE) + InputSymbol.END_OF_SEQUENCE -> + throw MalformedOpenPgpMessageException(State.OPENPGP_MESSAGE, input, stackItem) + else -> throw MalformedOpenPgpMessageException(State.OPENPGP_MESSAGE, input, stackItem) + } + } + + @Throws(MalformedOpenPgpMessageException::class) + fun fromLiteralMessage(input: InputSymbol, stackItem: StackSymbol?): Transition { + if (input == InputSymbol.SIGNATURE && stackItem == StackSymbol.OPS) { + return Transition(State.LITERAL_MESSAGE) + } + if (input == InputSymbol.END_OF_SEQUENCE && stackItem == StackSymbol.TERMINUS) { + return Transition(State.VALID) + } + + throw MalformedOpenPgpMessageException(State.LITERAL_MESSAGE, input, stackItem) + } + + @Throws(MalformedOpenPgpMessageException::class) + fun fromCompressedMessage(input: InputSymbol, stackItem: StackSymbol?): Transition { + if (input == InputSymbol.SIGNATURE && stackItem == StackSymbol.OPS) { + return Transition(State.COMPRESSED_MESSAGE) + } + if (input == InputSymbol.END_OF_SEQUENCE && stackItem == StackSymbol.TERMINUS) { + return Transition(State.VALID) + } + + throw MalformedOpenPgpMessageException(State.COMPRESSED_MESSAGE, input, stackItem) + } + + @Throws(MalformedOpenPgpMessageException::class) + fun fromEncryptedMessage(input: InputSymbol, stackItem: StackSymbol?): Transition { + if (input == InputSymbol.SIGNATURE && stackItem == StackSymbol.OPS) { + return Transition(State.ENCRYPTED_MESSAGE) + } + if (input == InputSymbol.END_OF_SEQUENCE && stackItem == StackSymbol.TERMINUS) { + return Transition(State.VALID) + } + + throw MalformedOpenPgpMessageException(State.ENCRYPTED_MESSAGE, input, stackItem) + } + + @Throws(MalformedOpenPgpMessageException::class) + fun fromValid(input: InputSymbol, stackItem: StackSymbol?): Transition { + if (input == InputSymbol.END_OF_SEQUENCE) { + // allow subsequent read() calls. + return Transition(State.VALID) + } + throw MalformedOpenPgpMessageException(State.VALID, input, stackItem) + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/PDA.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/PDA.kt new file mode 100644 index 00000000..7dff6ba2 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/PDA.kt @@ -0,0 +1,127 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification.syntax_check + +import org.pgpainless.exception.MalformedOpenPgpMessageException +import org.slf4j.LoggerFactory + +/** + * Pushdown Automaton for validating context-free languages. In PGPainless, this class is used to + * validate OpenPGP message packet sequences against the allowed syntax. + * + * See [OpenPGP Message Syntax](https://www.rfc-editor.org/rfc/rfc4880#section-11.3) + */ +class PDA +constructor( + private val syntax: Syntax, + private val stack: ArrayDeque, + private val inputs: MutableList, + private var state: State +) { + + /** + * Construct a PDA with a custom [Syntax], initial [State] and initial + * [StackSymbols][StackSymbol]. + * + * @param syntax syntax + * @param initialState initial state + * @param initialStack zero or more initial stack items (get pushed onto the stack in order of + * appearance) + */ + constructor( + syntax: Syntax, + initialState: State, + vararg initialStack: StackSymbol + ) : this(syntax, ArrayDeque(initialStack.toList().reversed()), mutableListOf(), initialState) + + /** Default constructor which initializes the PDA to work with the [OpenPgpMessageSyntax]. */ + constructor() : + this(OpenPgpMessageSyntax(), State.OPENPGP_MESSAGE, StackSymbol.TERMINUS, StackSymbol.MSG) + + /** + * Process the next [InputSymbol]. This will either leave the PDA in the next state, or throw a + * [MalformedOpenPgpMessageException] if the input symbol is rejected. + * + * @param input input symbol + * @throws MalformedOpenPgpMessageException if the input symbol is rejected + */ + fun next(input: InputSymbol) { + val stackSymbol = popStack() + try { + val transition = syntax.transition(state, input, stackSymbol) + state = transition.newState + for (item in transition.pushedItems) { + pushStack(item) + } + inputs.add(input) + } catch (e: MalformedOpenPgpMessageException) { + val stackFormat = + if (stackSymbol != null) { + "${stack.joinToString()}||$stackSymbol" + } else { + stack.joinToString() + } + val wrapped = + MalformedOpenPgpMessageException( + "Malformed message: After reading packet sequence ${inputs.joinToString()}, token '$input' is not allowed.\n" + + "No transition from state '$state' with stack $stackFormat", + e) + LOGGER.debug("Invalid input '$input'", wrapped) + throw wrapped + } + } + + /** + * Peek at the stack, returning the topmost stack item without changing the stack. + * + * @return topmost stack item, or null if stack is empty + */ + fun peekStack(): StackSymbol? = stack.firstOrNull() + + /** + * Return true, if the PDA is in a valid state (the OpenPGP message is valid). + * + * @return true if valid, false otherwise + */ + fun isValid(): Boolean = state == State.VALID && stack.isEmpty() + + /** + * Throw a [MalformedOpenPgpMessageException] if the pda is not in a valid state right now. + * + * @throws MalformedOpenPgpMessageException if the pda is not in an acceptable state + */ + fun assertValid() { + if (!isValid()) { + throw MalformedOpenPgpMessageException( + "Pushdown Automaton is not in an acceptable state: ${toString()}") + } + } + + /** + * Pop an item from the stack. + * + * @return stack item + */ + private fun popStack(): StackSymbol? { + return stack.removeFirstOrNull() + } + + /** + * Push an item onto the stack. + * + * @param item item + */ + private fun pushStack(item: StackSymbol) { + stack.addFirst(item) + } + + override fun toString(): String { + return "State: $state Stack: ${stack.joinToString()}" + } + + companion object { + @JvmStatic private val LOGGER = LoggerFactory.getLogger(PDA::class.java) + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/StackSymbol.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/StackSymbol.kt new file mode 100644 index 00000000..8f927864 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/StackSymbol.kt @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification.syntax_check + +enum class StackSymbol { + /** OpenPGP Message. */ + MSG, + /** OnePassSignature (in case of BC this represents a OnePassSignatureList). */ + OPS, + /** Special symbol representing the end of the message. */ + TERMINUS +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/State.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/State.kt new file mode 100644 index 00000000..8fad2fb7 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/State.kt @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification.syntax_check + +/** Set of states of the automaton. */ +enum class State { + OPENPGP_MESSAGE, + LITERAL_MESSAGE, + COMPRESSED_MESSAGE, + ENCRYPTED_MESSAGE, + VALID +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/Syntax.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/Syntax.kt new file mode 100644 index 00000000..a90e981e --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/Syntax.kt @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification.syntax_check + +import org.pgpainless.exception.MalformedOpenPgpMessageException + +/** This interface can be used to define a custom syntax for the [PDA]. */ +interface Syntax { + + /** + * Describe a transition rule from [State]
from
for [InputSymbol]
input
+ * with [StackSymbol]
stackItem
from the top of the [PDAs][PDA] stack. The resulting + * [Transition] contains the new [State], as well as a list of [StackSymbols][StackSymbol] that + * get pushed onto the stack by the transition rule. If there is no applicable rule, a + * [MalformedOpenPgpMessageException] is thrown, since in this case the [InputSymbol] must be + * considered illegal. + * + * @param from current state of the PDA + * @param input input symbol + * @param stackItem item that got popped from the top of the stack + * @return applicable transition rule containing the new state and pushed stack symbols + * @throws MalformedOpenPgpMessageException if there is no applicable transition rule (the input + * symbol is illegal) + */ + @Throws(MalformedOpenPgpMessageException::class) + fun transition(from: State, input: InputSymbol, stackItem: StackSymbol?): Transition +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/Transition.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/Transition.kt new file mode 100644 index 00000000..d0a09992 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/Transition.kt @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification.syntax_check + +/** + * Result of applying a transition rule. Transition rules can be described by implementing the + * [Syntax] interface. + * + * @param newState new [State] that is reached by applying the transition. + * @param pushedItems list of [StackSymbol] that are pushed onto the stack by applying the + * transition. The list contains items in the order in which they are pushed onto the stack. The + * list may be empty. + */ +class Transition private constructor(val pushedItems: List, val newState: State) { + + constructor( + newState: State, + vararg pushedItems: StackSymbol + ) : this(pushedItems.toList(), newState) +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/BcHashContextSigner.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/BcHashContextSigner.kt new file mode 100644 index 00000000..47aed2be --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/BcHashContextSigner.kt @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.encryption_signing + +import java.security.MessageDigest +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPPrivateKey +import org.bouncycastle.openpgp.PGPSecretKeyRing +import org.bouncycastle.openpgp.PGPSignature +import org.bouncycastle.openpgp.PGPSignatureGenerator +import org.pgpainless.PGPainless +import org.pgpainless.algorithm.SignatureType +import org.pgpainless.bouncycastle.extensions.unlock +import org.pgpainless.key.protection.SecretKeyRingProtector + +class BcHashContextSigner { + + companion object { + @JvmStatic + fun signHashContext( + hashContext: MessageDigest, + signatureType: SignatureType, + secretKey: PGPSecretKeyRing, + protector: SecretKeyRingProtector + ): PGPSignature { + val info = PGPainless.inspectKeyRing(secretKey) + return info.signingSubkeys + .mapNotNull { info.getSecretKey(it.keyID) } + .firstOrNull() + ?.let { signHashContext(hashContext, signatureType, it.unlock(protector)) } + ?: throw PGPException("Key does not contain suitable signing subkey.") + } + + /** + * Create an OpenPGP Signature over the given [MessageDigest] hash context. + * + * @param hashContext hash context + * @param privateKey signing-capable key + * @return signature + * @throws PGPException in case of an OpenPGP error + */ + @JvmStatic + internal fun signHashContext( + hashContext: MessageDigest, + signatureType: SignatureType, + privateKey: PGPPrivateKey + ): PGPSignature { + return PGPSignatureGenerator(BcPGPHashContextContentSignerBuilder(hashContext)) + .apply { init(signatureType.code, privateKey) } + .generate() + } + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/BcPGPHashContextContentSignerBuilder.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/BcPGPHashContextContentSignerBuilder.kt new file mode 100644 index 00000000..bf66b6dc --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/BcPGPHashContextContentSignerBuilder.kt @@ -0,0 +1,138 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.encryption_signing + +import java.io.OutputStream +import java.security.MessageDigest +import org.bouncycastle.bcpg.PublicKeyAlgorithmTags +import org.bouncycastle.crypto.CipherParameters +import org.bouncycastle.crypto.CryptoException +import org.bouncycastle.crypto.Digest +import org.bouncycastle.crypto.Signer +import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters +import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters +import org.bouncycastle.crypto.signers.DSADigestSigner +import org.bouncycastle.crypto.signers.DSASigner +import org.bouncycastle.crypto.signers.ECDSASigner +import org.bouncycastle.crypto.signers.Ed25519Signer +import org.bouncycastle.crypto.signers.Ed448Signer +import org.bouncycastle.crypto.signers.RSADigestSigner +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPPrivateKey +import org.bouncycastle.openpgp.operator.PGPContentSigner +import org.bouncycastle.openpgp.operator.bc.BcPGPKeyConverter +import org.pgpainless.algorithm.HashAlgorithm +import org.pgpainless.algorithm.PublicKeyAlgorithm + +/** + * Implementation of [PGPContentSignerBuilder] using the BC API, which can be used to sign hash + * contexts. This can come in handy to sign data, which was already processed to calculate the hash + * context, without the need to process it again to calculate the OpenPGP signature. + */ +class BcPGPHashContextContentSignerBuilder(private val messageDigest: MessageDigest) : + PGPHashContextContentSignerBuilder() { + + private val keyConverter = BcPGPKeyConverter() + private val _hashAlgorithm: HashAlgorithm + + init { + _hashAlgorithm = requireFromName(messageDigest.algorithm) + } + + override fun build(signatureType: Int, privateKey: PGPPrivateKey): PGPContentSigner { + val keyAlgorithm = PublicKeyAlgorithm.requireFromId(privateKey.publicKeyPacket.algorithm) + val privKeyParam = keyConverter.getPrivateKey(privateKey) + val signer = createSigner(keyAlgorithm, messageDigest, privKeyParam) + signer.init(true, privKeyParam) + + return object : PGPContentSigner { + override fun getOutputStream(): OutputStream = SignerOutputStream(signer) + + override fun getSignature(): ByteArray = + try { + signer.generateSignature() + } catch (e: CryptoException) { + throw IllegalStateException("unable to create signature.", e) + } + + override fun getDigest(): ByteArray = messageDigest.digest() + + override fun getType(): Int = signatureType + + override fun getHashAlgorithm(): Int = _hashAlgorithm.algorithmId + + override fun getKeyAlgorithm(): Int = keyAlgorithm.algorithmId + + override fun getKeyID(): Long = privateKey.keyID + } + } + + companion object { + @JvmStatic + private fun requireFromName(digestName: String): HashAlgorithm { + val algorithm = HashAlgorithm.fromName(digestName) + require(algorithm != null) { "Cannot recognize OpenPGP Hash Algorithm: $digestName" } + return algorithm + } + + @JvmStatic + private fun createSigner( + keyAlgorithm: PublicKeyAlgorithm, + messageDigest: MessageDigest, + keyParam: CipherParameters + ): Signer { + val staticDigest = ExistingMessageDigest(messageDigest) + return when (keyAlgorithm.algorithmId) { + PublicKeyAlgorithmTags.RSA_GENERAL, + PublicKeyAlgorithmTags.RSA_SIGN -> RSADigestSigner(staticDigest) + PublicKeyAlgorithmTags.DSA -> DSADigestSigner(DSASigner(), staticDigest) + PublicKeyAlgorithmTags.ECDSA -> DSADigestSigner(ECDSASigner(), staticDigest) + PublicKeyAlgorithmTags.EDDSA_LEGACY -> { + if (keyParam is Ed25519PrivateKeyParameters || + keyParam is Ed25519PublicKeyParameters) + EdDsaSigner(Ed25519Signer(), staticDigest) + else EdDsaSigner(Ed448Signer(byteArrayOf()), staticDigest) + } + else -> throw PGPException("cannot recognize keyAlgorithm: $keyAlgorithm") + } + } + } + + // Copied from BCs BcImplProvider - required since BCs class is package visible only :/ + internal class EdDsaSigner(private val signer: Signer, private val digest: Digest) : Signer { + private val digBuf: ByteArray = ByteArray(digest.digestSize) + + override fun init(forSigning: Boolean, param: CipherParameters) { + signer.init(forSigning, param) + digest.reset() + } + + override fun update(b: Byte) { + digest.update(b) + } + + override fun update(b: ByteArray, off: Int, len: Int) { + digest.update(b, off, len) + } + + override fun generateSignature(): ByteArray { + digest.doFinal(digBuf, 0) + signer.update(digBuf, 0, digBuf.size) + return signer.generateSignature() + } + + override fun verifySignature(signature: ByteArray): Boolean { + digest.doFinal(digBuf, 0) + signer.update(digBuf, 0, digBuf.size) + return signer.verifySignature(signature) + } + + override fun reset() { + digBuf.fill(0) + signer.reset() + digest.reset() + } + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/CRLFGeneratorStream.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/CRLFGeneratorStream.kt new file mode 100644 index 00000000..76d747e0 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/CRLFGeneratorStream.kt @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: 2021 David Hook +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.encryption_signing + +import java.io.OutputStream +import org.pgpainless.algorithm.StreamEncoding + +/** + * [OutputStream] which applies CR-LF encoding of its input data, based on the desired + * [StreamEncoding]. This implementation originates from the Bouncy Castle library. + */ +class CRLFGeneratorStream(private val crlfOut: OutputStream, encoding: StreamEncoding) : + OutputStream() { + + private val isBinary: Boolean + private var lastB = 0 + + init { + isBinary = encoding == StreamEncoding.BINARY + } + + override fun write(b: Int) { + if (!isBinary) { + if (b == '\n'.code && lastB != '\r'.code) { // Unix + crlfOut.write('\r'.code) + } else if (lastB == '\r'.code) { // MAC + if (b != '\n'.code) { + crlfOut.write('\n'.code) + } + } + lastB = b + } + crlfOut.write(b) + } + + override fun close() { + if (!isBinary && lastB == '\r'.code) { + crlfOut.write('\n'.code) + } + crlfOut.close() + } + + override fun flush() { + super.flush() + crlfOut.flush() + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionBuilder.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionBuilder.kt new file mode 100644 index 00000000..6b4713d6 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionBuilder.kt @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.encryption_signing + +import java.io.OutputStream +import org.pgpainless.PGPainless.Companion.getPolicy +import org.pgpainless.algorithm.CompressionAlgorithm +import org.pgpainless.algorithm.SymmetricKeyAlgorithm +import org.pgpainless.algorithm.negotiation.SymmetricKeyAlgorithmNegotiator.Companion.byPopularity +import org.pgpainless.util.NullOutputStream +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +class EncryptionBuilder : EncryptionBuilderInterface { + override fun onOutputStream( + outputStream: OutputStream + ): EncryptionBuilderInterface.WithOptions { + return WithOptionsImpl(outputStream) + } + + override fun discardOutput(): EncryptionBuilderInterface.WithOptions { + return onOutputStream(NullOutputStream()) + } + + class WithOptionsImpl(val outputStream: OutputStream) : EncryptionBuilderInterface.WithOptions { + + override fun withOptions(options: ProducerOptions): EncryptionStream { + return EncryptionStream(outputStream, options) + } + } + + companion object { + + @JvmStatic val LOGGER: Logger = LoggerFactory.getLogger(EncryptionBuilder::class.java) + + /** + * Negotiate the [SymmetricKeyAlgorithm] used for message encryption. + * + * @param encryptionOptions encryption options + * @return negotiated symmetric key algorithm + */ + @JvmStatic + fun negotiateSymmetricEncryptionAlgorithm( + encryptionOptions: EncryptionOptions + ): SymmetricKeyAlgorithm { + val preferences = + encryptionOptions.keyViews.values + .map { it.preferredSymmetricKeyAlgorithms } + .toList() + val algorithm = + byPopularity() + .negotiate( + getPolicy().symmetricKeyEncryptionAlgorithmPolicy, + encryptionOptions.encryptionAlgorithmOverride, + preferences) + LOGGER.debug( + "Negotiation resulted in {} being the symmetric encryption algorithm of choice.", + algorithm) + return algorithm + } + + @JvmStatic + fun negotiateCompressionAlgorithm(producerOptions: ProducerOptions): CompressionAlgorithm { + val compressionAlgorithmOverride = producerOptions.compressionAlgorithmOverride + return compressionAlgorithmOverride + ?: getPolicy().compressionAlgorithmPolicy.defaultCompressionAlgorithm() + + // TODO: Negotiation + } + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionBuilderInterface.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionBuilderInterface.kt new file mode 100644 index 00000000..2db98846 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionBuilderInterface.kt @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.encryption_signing + +import java.io.IOException +import java.io.OutputStream +import org.bouncycastle.openpgp.PGPException + +interface EncryptionBuilderInterface { + + /** + * Create a [EncryptionStream] wrapping an [OutputStream]. Data that is piped through the + * [EncryptionStream] will be encrypted and/or signed. + * + * @param outputStream output stream which receives the encrypted / signed data. + * @return api handle + */ + fun onOutputStream(outputStream: OutputStream): WithOptions + + /** + * Create an [EncryptionStream] that discards the data after processing it. This is useful, e.g. + * for generating detached signatures, where the resulting signature is retrieved from the + * [EncryptionResult] once the operation is finished. In this case, the plaintext data does not + * need to be retained. + * + * @return api handle + */ + fun discardOutput(): WithOptions + + fun interface WithOptions { + + /** + * Create an [EncryptionStream] with the given options (recipients, signers, algorithms...). + * + * @param options options + * @return encryption stream + * @throws PGPException if something goes wrong during encryption stream preparation + * @throws IOException if something goes wrong during encryption stream preparation (writing + * headers) + */ + @Throws(PGPException::class, IOException::class) + fun withOptions(options: ProducerOptions): EncryptionStream + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionOptions.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionOptions.kt new file mode 100644 index 00000000..f261b85e --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionOptions.kt @@ -0,0 +1,325 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.encryption_signing + +import java.util.* +import org.bouncycastle.openpgp.PGPPublicKey +import org.bouncycastle.openpgp.PGPPublicKeyRing +import org.bouncycastle.openpgp.operator.PGPKeyEncryptionMethodGenerator +import org.pgpainless.algorithm.EncryptionPurpose +import org.pgpainless.algorithm.SymmetricKeyAlgorithm +import org.pgpainless.authentication.CertificateAuthority +import org.pgpainless.encryption_signing.EncryptionOptions.EncryptionKeySelector +import org.pgpainless.exception.KeyException +import org.pgpainless.exception.KeyException.* +import org.pgpainless.implementation.ImplementationFactory +import org.pgpainless.key.OpenPgpFingerprint +import org.pgpainless.key.SubkeyIdentifier +import org.pgpainless.key.info.KeyAccessor +import org.pgpainless.key.info.KeyRingInfo +import org.pgpainless.util.Passphrase + +class EncryptionOptions(private val purpose: EncryptionPurpose) { + private val _encryptionMethods: MutableSet = mutableSetOf() + private val _encryptionKeyIdentifiers: MutableSet = mutableSetOf() + private val _keyRingInfo: MutableMap = mutableMapOf() + private val _keyViews: MutableMap = mutableMapOf() + private val encryptionKeySelector: EncryptionKeySelector = encryptToAllCapableSubkeys() + + private var allowEncryptionWithMissingKeyFlags = false + private var evaluationDate = Date() + private var _encryptionAlgorithmOverride: SymmetricKeyAlgorithm? = null + + val encryptionMethods + get() = _encryptionMethods.toSet() + + val encryptionKeyIdentifiers + get() = _encryptionKeyIdentifiers.toSet() + + val keyRingInfo + get() = _keyRingInfo.toMap() + + val keyViews + get() = _keyViews.toMap() + + val encryptionAlgorithmOverride + get() = _encryptionAlgorithmOverride + + constructor() : this(EncryptionPurpose.ANY) + + /** + * Factory method to create an [EncryptionOptions] object which will encrypt for keys which + * carry the flag [org.pgpainless.algorithm.KeyFlag.ENCRYPT_COMMS]. + * + * @return encryption options + */ + fun setEvaluationDate(evaluationDate: Date) = apply { this.evaluationDate = evaluationDate } + + /** + * Identify authenticatable certificates for the given user-ID by querying the + * [CertificateAuthority] for identifiable bindings. Add all acceptable bindings, whose trust + * amount is larger or equal to the target amount to the list of recipients. + * + * @param userId userId + * @param email if true, treat the user-ID as an email address and match all user-IDs containing + * the mail address + * @param authority certificate authority + * @param targetAmount target amount (120 = fully authenticated, 240 = doubly authenticated, 60 + * = partially authenticated...) + * @return encryption options + */ + @JvmOverloads + fun addAuthenticatableRecipients( + userId: String, + email: Boolean, + authority: CertificateAuthority, + targetAmount: Int = 120 + ) = apply { + var foundAcceptable = false + authority + .lookupByUserId(userId, email, evaluationDate, targetAmount) + .filter { it.isAuthenticated() } + .forEach { addRecipient(it.certificate).also { foundAcceptable = true } } + require(foundAcceptable) { + "Could not identify any trust-worthy certificates for '$userId' and target trust amount $targetAmount." + } + } + + /** + * Add all key rings in the provided [Iterable] (e.g. + * [org.bouncycastle.openpgp.PGPPublicKeyRingCollection]) as recipients. + * + * @param keys keys + * @return this + */ + fun addRecipients(keys: Iterable) = apply { + keys.toList().let { + require(it.isNotEmpty()) { "Set of recipient keys cannot be empty." } + it.forEach { key -> addRecipient(key) } + } + } + + /** + * Add all key rings in the provided [Iterable] (e.g. + * [org.bouncycastle.openpgp.PGPPublicKeyRingCollection]) as recipients. Per key ring, the + * selector is applied to select one or more encryption subkeys. + * + * @param keys keys + * @param selector encryption key selector + * @return this + */ + fun addRecipients(keys: Iterable, selector: EncryptionKeySelector) = apply { + keys.toList().let { + require(it.isNotEmpty()) { "Set of recipient keys cannot be empty." } + it.forEach { key -> addRecipient(key, selector) } + } + } + + /** + * Add a recipient by providing a key. + * + * @param key key ring + * @return this + */ + fun addRecipient(key: PGPPublicKeyRing) = addRecipient(key, encryptionKeySelector) + + /** + * Add a recipient by providing a key and recipient user-id. The user-id is used to determine + * the recipients preferences (algorithms etc.). + * + * @param key key ring + * @param userId user id + * @return this + */ + fun addRecipient(key: PGPPublicKeyRing, userId: CharSequence) = + addRecipient(key, userId, encryptionKeySelector) + + fun addRecipient( + key: PGPPublicKeyRing, + userId: CharSequence, + encryptionKeySelector: EncryptionKeySelector + ) = apply { + val info = KeyRingInfo(key, evaluationDate) + val subkeys = + encryptionKeySelector.selectEncryptionSubkeys( + info.getEncryptionSubkeys(userId, purpose)) + if (subkeys.isEmpty()) { + throw KeyException.UnacceptableEncryptionKeyException(OpenPgpFingerprint.of(key)) + } + + for (subkey in subkeys) { + val keyId = SubkeyIdentifier(key, subkey.keyID) + _keyRingInfo[keyId] = info + _keyViews[keyId] = KeyAccessor.ViaUserId(info, keyId, userId.toString()) + addRecipientKey(key, subkey, false) + } + } + + fun addRecipient(key: PGPPublicKeyRing, encryptionKeySelector: EncryptionKeySelector) = apply { + addAsRecipient(key, encryptionKeySelector, false) + } + + @JvmOverloads + fun addHiddenRecipient( + key: PGPPublicKeyRing, + selector: EncryptionKeySelector = encryptionKeySelector + ) = apply { addAsRecipient(key, selector, true) } + + private fun addAsRecipient( + key: PGPPublicKeyRing, + selector: EncryptionKeySelector, + wildcardKeyId: Boolean + ) = apply { + val info = KeyRingInfo(key, evaluationDate) + val primaryKeyExpiration = + try { + info.primaryKeyExpirationDate + } catch (e: NoSuchElementException) { + throw UnacceptableSelfSignatureException(OpenPgpFingerprint.of(key)) + } + + if (primaryKeyExpiration != null && primaryKeyExpiration < evaluationDate) { + throw ExpiredKeyException(OpenPgpFingerprint.of(key), primaryKeyExpiration) + } + + var encryptionSubkeys = selector.selectEncryptionSubkeys(info.getEncryptionSubkeys(purpose)) + + // There are some legacy keys around without key flags. + // If we allow encryption for those keys, we add valid keys without any key flags, if they + // are + // capable of encryption by means of their algorithm + if (encryptionSubkeys.isEmpty() && allowEncryptionWithMissingKeyFlags) { + encryptionSubkeys = + info.validSubkeys + .filter { it.isEncryptionKey } + .filter { info.getKeyFlagsOf(it.keyID).isEmpty() } + } + + if (encryptionSubkeys.isEmpty()) { + throw UnacceptableEncryptionKeyException(OpenPgpFingerprint.of(key)) + } + + for (subkey in encryptionSubkeys) { + val keyId = SubkeyIdentifier(key, subkey.keyID) + _keyRingInfo[keyId] = info + _keyViews[keyId] = KeyAccessor.ViaKeyId(info, keyId) + addRecipientKey(key, subkey, wildcardKeyId) + } + } + + private fun addRecipientKey( + certificate: PGPPublicKeyRing, + key: PGPPublicKey, + wildcardKeyId: Boolean + ) { + _encryptionKeyIdentifiers.add(SubkeyIdentifier(certificate, key.keyID)) + addEncryptionMethod( + ImplementationFactory.getInstance().getPublicKeyKeyEncryptionMethodGenerator(key).also { + it.setUseWildcardKeyID(wildcardKeyId) + }) + } + + /** + * Add a symmetric passphrase which the message will be encrypted to. + * + * @param passphrase passphrase + * @return this + */ + @Deprecated( + "Deprecated in favor of addMessagePassphrase", + ReplaceWith("addMessagePassphrase(passphrase)")) + fun addPassphrase(passphrase: Passphrase) = addMessagePassphrase(passphrase) + + /** + * Add a symmetric passphrase which the message will be encrypted to. + * + * @param passphrase passphrase + * @return this + */ + fun addMessagePassphrase(passphrase: Passphrase) = apply { + require(!passphrase.isEmpty) { "Passphrase MUST NOT be empty." } + addEncryptionMethod( + ImplementationFactory.getInstance().getPBEKeyEncryptionMethodGenerator(passphrase)) + } + + /** + * Add a [PGPKeyEncryptionMethodGenerator] which will be used to encrypt the message. Method + * generators are either [org.bouncycastle.openpgp.operator.PBEKeyEncryptionMethodGenerator] + * (passphrase) or [PGPKeyEncryptionMethodGenerator] (public key). + * + * This method is intended for advanced users to allow encryption for specific subkeys. This can + * come in handy for example if data needs to be encrypted to a subkey that's ignored by + * PGPainless. + * + * @param encryptionMethod encryption method + * @return this + */ + fun addEncryptionMethod(encryptionMethod: PGPKeyEncryptionMethodGenerator) = apply { + _encryptionMethods.add(encryptionMethod) + } + + /** + * Override the used symmetric encryption algorithm. The symmetric encryption algorithm is used + * to encrypt the message itself, while the used symmetric key will be encrypted to all + * recipients using public key cryptography. + * + * If the algorithm is not overridden, a suitable algorithm will be negotiated. + * + * @param encryptionAlgorithm encryption algorithm override + * @return this + */ + fun overrideEncryptionAlgorithm(encryptionAlgorithm: SymmetricKeyAlgorithm) = apply { + require(encryptionAlgorithm != SymmetricKeyAlgorithm.NULL) { + "Encryption algorithm override cannot be NULL." + } + _encryptionAlgorithmOverride = encryptionAlgorithm + } + + /** + * If this method is called, subsequent calls to [addRecipient] will allow encryption for + * subkeys that do not carry any [org.pgpainless.algorithm.KeyFlag] subpacket. This is a + * workaround for dealing with legacy keys that have no key flags subpacket but rely on the key + * algorithm type to convey the subkeys use. + * + * @return this + */ + fun setAllowEncryptionWithMissingKeyFlags() = apply { + this.allowEncryptionWithMissingKeyFlags = true + } + + fun hasEncryptionMethod() = _encryptionMethods.isNotEmpty() + + fun interface EncryptionKeySelector { + fun selectEncryptionSubkeys(encryptionCapableKeys: List): List + } + + companion object { + @JvmStatic fun get() = EncryptionOptions() + + @JvmStatic fun encryptCommunications() = EncryptionOptions(EncryptionPurpose.COMMUNICATIONS) + + @JvmStatic fun encryptDataAtRest() = EncryptionOptions(EncryptionPurpose.STORAGE) + + /** + * Only encrypt to the first valid encryption capable subkey we stumble upon. + * + * @return encryption key selector + */ + @JvmStatic + fun encryptToFirstSubkey() = EncryptionKeySelector { encryptionCapableKeys -> + encryptionCapableKeys.firstOrNull()?.let { listOf(it) } ?: listOf() + } + + /** + * Encrypt to any valid, encryption capable subkey on the key ring. + * + * @return encryption key selector + */ + @JvmStatic + fun encryptToAllCapableSubkeys() = EncryptionKeySelector { encryptionCapableKeys -> + encryptionCapableKeys + } + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionResult.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionResult.kt new file mode 100644 index 00000000..4f6e6978 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionResult.kt @@ -0,0 +1,103 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.encryption_signing + +import java.util.* +import org.bouncycastle.openpgp.PGPLiteralData +import org.bouncycastle.openpgp.PGPPublicKeyRing +import org.bouncycastle.openpgp.PGPSignature +import org.pgpainless.algorithm.CompressionAlgorithm +import org.pgpainless.algorithm.StreamEncoding +import org.pgpainless.algorithm.SymmetricKeyAlgorithm +import org.pgpainless.bouncycastle.extensions.matches +import org.pgpainless.key.SubkeyIdentifier +import org.pgpainless.util.MultiMap + +data class EncryptionResult( + val encryptionAlgorithm: SymmetricKeyAlgorithm, + val compressionAlgorithm: CompressionAlgorithm, + val detachedSignatures: MultiMap, + val recipients: Set, + val fileName: String, + val modificationDate: Date, + val fileEncoding: StreamEncoding +) { + + /** + * Return true, if the message is marked as for-your-eyes-only. This is typically done by + * setting the filename "_CONSOLE". + * + * @return is message for your eyes only? + */ + val isForYourEyesOnly: Boolean + get() = PGPLiteralData.CONSOLE == fileName + + /** + * Returns true, if the message was encrypted for at least one subkey of the given certificate. + * + * @param certificate certificate + * @return true if encrypted for 1+ subkeys, false otherwise. + */ + fun isEncryptedFor(certificate: PGPPublicKeyRing) = recipients.any { certificate.matches(it) } + + companion object { + /** + * Create a builder for the encryption result class. + * + * @return builder + */ + @JvmStatic fun builder() = Builder() + } + + class Builder { + var _encryptionAlgorithm: SymmetricKeyAlgorithm? = null + var _compressionAlgorithm: CompressionAlgorithm? = null + + val detachedSignatures: MultiMap = MultiMap() + val recipients: Set = mutableSetOf() + private var _fileName = "" + private var _modificationDate = Date(0) + private var _encoding = StreamEncoding.BINARY + + fun setEncryptionAlgorithm(encryptionAlgorithm: SymmetricKeyAlgorithm) = apply { + _encryptionAlgorithm = encryptionAlgorithm + } + + fun setCompressionAlgorithm(compressionAlgorithm: CompressionAlgorithm) = apply { + _compressionAlgorithm = compressionAlgorithm + } + + fun setFileName(fileName: String) = apply { _fileName = fileName } + + fun setModificationDate(modificationDate: Date) = apply { + _modificationDate = modificationDate + } + + fun setFileEncoding(encoding: StreamEncoding) = apply { _encoding = encoding } + + fun addRecipient(recipient: SubkeyIdentifier) = apply { + (recipients as MutableSet).add(recipient) + } + + fun addDetachedSignature( + signingSubkeyIdentifier: SubkeyIdentifier, + detachedSignature: PGPSignature + ) = apply { detachedSignatures.put(signingSubkeyIdentifier, detachedSignature) } + + fun build(): EncryptionResult { + checkNotNull(_encryptionAlgorithm) { "Encryption algorithm not set." } + checkNotNull(_compressionAlgorithm) { "Compression algorithm not set." } + + return EncryptionResult( + _encryptionAlgorithm!!, + _compressionAlgorithm!!, + detachedSignatures, + recipients, + _fileName, + _modificationDate, + _encoding) + } + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionStream.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionStream.kt new file mode 100644 index 00000000..f2617c34 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionStream.kt @@ -0,0 +1,273 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.encryption_signing + +import java.io.BufferedOutputStream +import java.io.IOException +import java.io.OutputStream +import org.bouncycastle.bcpg.ArmoredOutputStream +import org.bouncycastle.bcpg.BCPGOutputStream +import org.bouncycastle.openpgp.PGPCompressedDataGenerator +import org.bouncycastle.openpgp.PGPEncryptedDataGenerator +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPLiteralDataGenerator +import org.pgpainless.algorithm.CompressionAlgorithm +import org.pgpainless.algorithm.StreamEncoding +import org.pgpainless.algorithm.SymmetricKeyAlgorithm +import org.pgpainless.implementation.ImplementationFactory +import org.pgpainless.util.ArmoredOutputStreamFactory +import org.slf4j.LoggerFactory + +// 1 << 8 causes wrong partial body length encoding +// 1 << 9 fixes this. +// see https://github.com/pgpainless/pgpainless/issues/160 +const val BUFFER_SIZE = 1 shl 9 + +/** + * OutputStream that produces an OpenPGP message. The message can be encrypted, signed, or both, + * depending on its configuration. + * + * This class is based upon Jens Neuhalfen's Bouncy-GPG PGPEncryptingStream. + * + * @see Source + */ +class EncryptionStream( + private var outermostStream: OutputStream, + private val options: ProducerOptions, +) : OutputStream() { + + private val resultBuilder: EncryptionResult.Builder = EncryptionResult.builder() + private var closed: Boolean = false + + private var signatureLayerStream: OutputStream? = null + private var armorOutputStream: ArmoredOutputStream? = null + private var publicKeyEncryptedStream: OutputStream? = null + private var compressedDataGenerator: PGPCompressedDataGenerator? = null + private var basicCompressionStream: BCPGOutputStream? = null + private var literalDataGenerator: PGPLiteralDataGenerator? = null + private var literalDataStream: OutputStream? = null + + init { + prepareArmor() + prepareEncryption() + prepareCompression() + prepareOnePassSignatures() + prepareLiteralDataProcessing() + prepareSigningStream() + prepareInputEncoding() + } + + private fun prepareArmor() { + if (!options.isAsciiArmor) { + LOGGER.debug("Output will be unarmored.") + return + } + + outermostStream = BufferedOutputStream(outermostStream) + LOGGER.debug("Wrap encryption output in ASCII armor.") + armorOutputStream = + ArmoredOutputStreamFactory.get(outermostStream, options).also { outermostStream = it } + } + + @Throws(IOException::class, PGPException::class) + private fun prepareEncryption() { + if (options.encryptionOptions == null) { + // No encryption options -> no encryption + resultBuilder.setEncryptionAlgorithm(SymmetricKeyAlgorithm.NULL) + return + } + require(options.encryptionOptions.encryptionMethods.isNotEmpty()) { + "If EncryptionOptions are provided, at least one encryption method MUST be provided as well." + } + + EncryptionBuilder.negotiateSymmetricEncryptionAlgorithm(options.encryptionOptions).let { + resultBuilder.setEncryptionAlgorithm(it) + LOGGER.debug("Encrypt message using symmetric algorithm $it.") + val encryptedDataGenerator = + PGPEncryptedDataGenerator( + ImplementationFactory.getInstance().getPGPDataEncryptorBuilder(it).apply { + setWithIntegrityPacket(true) + }) + options.encryptionOptions.encryptionMethods.forEach { m -> + encryptedDataGenerator.addMethod(m) + } + options.encryptionOptions.encryptionKeyIdentifiers.forEach { r -> + resultBuilder.addRecipient(r) + } + + publicKeyEncryptedStream = + encryptedDataGenerator.open(outermostStream, ByteArray(BUFFER_SIZE)).also { stream + -> + outermostStream = stream + } + } + } + + @Throws(IOException::class) + private fun prepareCompression() { + EncryptionBuilder.negotiateCompressionAlgorithm(options).let { + resultBuilder.setCompressionAlgorithm(it) + compressedDataGenerator = PGPCompressedDataGenerator(it.algorithmId) + if (it == CompressionAlgorithm.UNCOMPRESSED) return + + LOGGER.debug("Compress using $it.") + basicCompressionStream = + BCPGOutputStream(compressedDataGenerator!!.open(outermostStream)).also { stream -> + outermostStream = stream + } + } + } + + @Throws(IOException::class, PGPException::class) + private fun prepareOnePassSignatures() { + signatureLayerStream = outermostStream + if (options.signingOptions == null) { + return + } + require(options.signingOptions.signingMethods.isNotEmpty()) { + "If SigningOptions are provided, at least one SigningMethod MUST be provided." + } + for ((index, method) in options.signingOptions.signingMethods.values.withIndex()) { + if (!method.isDetached) { + // The last sig is not nested, all others are + val nested = index + 1 < options.signingOptions.signingMethods.size + method.signatureGenerator.generateOnePassVersion(nested).encode(outermostStream) + } + } + } + + @Throws(IOException::class) + private fun prepareLiteralDataProcessing() { + if (options.isCleartextSigned) { + val hashAlgorithms = collectHashAlgorithmsForCleartextSigning() + armorOutputStream!!.beginClearText(*hashAlgorithms.toIntArray()) + return + } + + literalDataGenerator = + PGPLiteralDataGenerator().also { gen -> + literalDataStream = + gen.open( + outermostStream, + options.encoding.code, + options.fileName, + options.modificationDate, + ByteArray(BUFFER_SIZE)) + .also { stream -> outermostStream = stream } + } + resultBuilder.apply { + setFileName(options.fileName) + setModificationDate(options.modificationDate) + setFileEncoding(options.encoding) + } + } + + private fun prepareSigningStream() { + outermostStream = SignatureGenerationStream(outermostStream, options.signingOptions) + } + + private fun prepareInputEncoding() { + outermostStream = + CRLFGeneratorStream( + // By buffering here, we drastically improve performance + // Reason is that CRLFGeneratorStream only implements write(int), so we need + // BufferedOutputStream to + // "convert" to write(buf) calls again + BufferedOutputStream(outermostStream), + if (options.isApplyCRLFEncoding) StreamEncoding.UTF8 else StreamEncoding.BINARY) + } + + private fun collectHashAlgorithmsForCleartextSigning(): Array { + return options.signingOptions + ?.signingMethods + ?.values + ?.map { it.hashAlgorithm } + ?.toSet() + ?.map { it.algorithmId } + ?.toTypedArray() + ?: arrayOf() + } + + @Throws(IOException::class) override fun write(data: Int) = outermostStream.write(data) + + @Throws(IOException::class) + override fun write(buffer: ByteArray) = write(buffer, 0, buffer.size) + + @Throws(IOException::class) + override fun write(buffer: ByteArray, off: Int, len: Int) = + outermostStream.write(buffer, off, len) + + @Throws(IOException::class) override fun flush() = outermostStream.flush() + + @Throws(IOException::class) + override fun close() { + if (closed) return + + outermostStream.close() + literalDataStream?.apply { + flush() + close() + } + literalDataGenerator?.close() + + if (options.isCleartextSigned) { + armorOutputStream?.apply { + write('\r'.code) + write('\n'.code) + endClearText() + } + } + + try { + writeSignatures() + } catch (e: PGPException) { + throw IOException("Exception while writing signatures.", e) + } + + compressedDataGenerator?.close() + + publicKeyEncryptedStream?.apply { + flush() + close() + } + + armorOutputStream?.apply { + flush() + close() + } + closed = true + } + + @Throws(PGPException::class, IOException::class) + private fun writeSignatures() { + if (options.signingOptions == null) { + return + } + + options.signingOptions.signingMethods.entries.reversed().forEach { (key, method) -> + method.signatureGenerator.generate().let { sig -> + if (method.isDetached) { + resultBuilder.addDetachedSignature(key, sig) + } + if (!method.isDetached || options.isCleartextSigned) { + sig.encode(signatureLayerStream) + } + } + } + } + + val result: EncryptionResult + get() = + check(closed) { "EncryptionStream must be closed before accessing the result." } + .let { resultBuilder.build() } + + val isClosed + get() = closed + + companion object { + @JvmStatic private val LOGGER = LoggerFactory.getLogger(EncryptionStream::class.java) + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/PGPHashContextContentSignerBuilder.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/PGPHashContextContentSignerBuilder.kt new file mode 100644 index 00000000..ef8641a7 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/PGPHashContextContentSignerBuilder.kt @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.encryption_signing + +import java.io.OutputStream +import java.security.MessageDigest +import org.bouncycastle.crypto.Digest +import org.bouncycastle.crypto.Signer +import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder + +abstract class PGPHashContextContentSignerBuilder : PGPContentSignerBuilder { + + // Copied from BC, required since BCs class is package visible only + internal class SignerOutputStream(private val signer: Signer) : OutputStream() { + override fun write(p0: Int) = signer.update(p0.toByte()) + + override fun write(b: ByteArray) = signer.update(b, 0, b.size) + + override fun write(b: ByteArray, off: Int, len: Int) = signer.update(b, off, len) + } + + internal class ExistingMessageDigest(private val digest: MessageDigest) : Digest { + + override fun getAlgorithmName(): String = digest.algorithm + + override fun getDigestSize(): Int = digest.digestLength + + override fun update(b: Byte) = digest.update(b) + + override fun update(buf: ByteArray, inOff: Int, len: Int) = digest.update(buf) + + override fun doFinal(out: ByteArray, outOff: Int): Int { + digest.digest().copyInto(out, outOff) + return digestSize + } + + override fun reset() { + // Nope! + // We cannot reset, since BCs signer classes are resetting in their init() methods, + // which would also reset + // the messageDigest, losing its state. This would shatter our intention. + } + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/ProducerOptions.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/ProducerOptions.kt new file mode 100644 index 00000000..e77ef2e3 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/ProducerOptions.kt @@ -0,0 +1,269 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.encryption_signing + +import java.util.* +import org.bouncycastle.openpgp.PGPLiteralData +import org.pgpainless.PGPainless +import org.pgpainless.algorithm.CompressionAlgorithm +import org.pgpainless.algorithm.StreamEncoding + +class ProducerOptions +private constructor( + val encryptionOptions: EncryptionOptions?, + val signingOptions: SigningOptions? +) { + + private var _fileName: String = "" + private var _modificationDate: Date = PGPLiteralData.NOW + private var encodingField: StreamEncoding = StreamEncoding.BINARY + private var applyCRLFEncoding = false + private var cleartextSigned = false + private var _hideArmorHeaders = false + var isDisableAsciiArmorCRC = false + + private var _compressionAlgorithmOverride: CompressionAlgorithm = + PGPainless.getPolicy().compressionAlgorithmPolicy.defaultCompressionAlgorithm + private var asciiArmor = true + private var _comment: String? = null + private var _version: String? = null + + /** + * Specify, whether the result of the encryption/signing operation shall be ascii armored. The + * default value is true. + * + * @param asciiArmor ascii armor + * @return builder + */ + fun setAsciiArmor(asciiArmor: Boolean) = apply { + require(!(cleartextSigned && !asciiArmor)) { + "Cleartext signing is enabled. Cannot disable ASCII armoring." + } + this.asciiArmor = asciiArmor + } + + /** + * Return true if the output of the encryption/signing operation shall be ascii armored. + * + * @return ascii armored + */ + val isAsciiArmor: Boolean + get() = asciiArmor + + /** + * Set the comment header in ASCII armored output. The default value is null, which means no + * comment header is added. Multiline comments are possible using '\\n'.
Note: If a default + * header comment is set using [org.pgpainless.util.ArmoredOutputStreamFactory.setComment], then + * both comments will be written to the produced ASCII armor. + * + * @param comment comment header text + * @return builder + */ + fun setComment(comment: String?) = apply { _comment = comment } + + /** + * Return comment set for header in ascii armored output. + * + * @return comment + */ + val comment: String? + get() = _comment + + /** + * Return whether a comment was set (!= null). + * + * @return true if commend is set + */ + fun hasComment() = _comment != null + + /** + * Set the version header in ASCII armored output. The default value is null, which means no + * version header is added.
Note: If the value is non-null, then this method overrides the + * default version header set using + * [org.pgpainless.util.ArmoredOutputStreamFactory.setVersionInfo]. + * + * @param version version header, or null for no version info. + * @return builder + */ + fun setVersion(version: String?) = apply { _version = version } + + /** + * Return the version info header in ascii armored output. + * + * @return version info + */ + val version: String? + get() = _version + + /** + * Return whether a version header was set (!= null). + * + * @return true if version header is set + */ + fun hasVersion() = version != null + + fun setCleartextSigned() = apply { + require(signingOptions != null) { + "Signing Options cannot be null if cleartext signing is enabled." + } + require(encryptionOptions == null) { + "Cannot encode encrypted message as Cleartext Signed." + } + require(signingOptions.signingMethods.values.all { it.isDetached }) { + "For cleartext signed messages, all signatures must be added as detached signatures." + } + + cleartextSigned = true + asciiArmor = true + _compressionAlgorithmOverride = CompressionAlgorithm.UNCOMPRESSED + } + + val isCleartextSigned: Boolean + get() = cleartextSigned + + /** + * Set the name of the encrypted file. Note: This option cannot be used simultaneously with + * [setForYourEyesOnly]. + * + * @param fileName name of the encrypted file + * @return this + */ + fun setFileName(fileName: String) = apply { _fileName = fileName } + + /** + * Return the encrypted files name. + * + * @return file name + */ + val fileName: String + get() = _fileName + + /** + * Mark the encrypted message as for-your-eyes-only by setting a special file name. Note: + * Therefore this method cannot be used simultaneously with [setFileName]. + * + * @return this + * @deprecated deprecated since at least crypto-refresh-05. It is not recommended using this + * special filename in newly generated literal data packets + */ + @Deprecated("Signaling using special file name is discouraged.") + fun setForYourEyesOnly() = apply { _fileName = PGPLiteralData.CONSOLE } + + /** + * Set the modification date of the encrypted file. + * + * @param modificationDate Modification date of the encrypted file. + * @return this + */ + fun setModificationDate(modificationDate: Date) = apply { _modificationDate = modificationDate } + + /** + * Return the modification date of the encrypted file. + * + * @return modification date + */ + val modificationDate: Date + get() = _modificationDate + + /** + * Set format metadata field of the literal data packet. Defaults to [StreamEncoding.BINARY]. + *
This does not change the encoding of the wrapped data itself. To apply CR/LF encoding to + * your input data before processing, use [applyCRLFEncoding] instead. + * + * @param encoding encoding + * @return this + * @see RFC4880 §5.9. + * Literal Data Packet + * @deprecated options other than the default value of [StreamEncoding.BINARY] are discouraged. + */ + @Deprecated("Options other than BINARY are discouraged.") + fun setEncoding(encoding: StreamEncoding) = apply { encodingField = encoding } + + val encoding: StreamEncoding + get() = encodingField + + /** + * Apply special encoding of line endings to the input data. By default, this is disabled, which + * means that the data is not altered.
Enabling it will change the line endings to CR/LF. + * Note: The encoding will not be reversed when decrypting, so applying CR/LF encoding will + * result in the identity "decrypt(encrypt(data)) == data == verify(sign(data))". + * + * @return this + */ + fun applyCRLFEncoding() = apply { applyCRLFEncoding = true } + + /** + * Return the input encoding that will be applied before signing / encryption. + * + * @return input encoding + */ + val isApplyCRLFEncoding: Boolean + get() = applyCRLFEncoding + + /** + * Override which compression algorithm shall be used. + * + * @param compressionAlgorithm compression algorithm override + * @return builder + */ + fun overrideCompressionAlgorithm(compressionAlgorithm: CompressionAlgorithm) = apply { + _compressionAlgorithmOverride = compressionAlgorithm + } + + val compressionAlgorithmOverride: CompressionAlgorithm + get() = _compressionAlgorithmOverride + + val isHideArmorHeaders: Boolean + get() = _hideArmorHeaders + + /** + * If set to `true`, armor headers like version or comments will be omitted from armored output. + * By default, armor headers are not hidden. Note: If comments are added via [setComment], those + * are not omitted, even if [hideArmorHeaders] is set to `true`. + * + * @param hideArmorHeaders true or false + * @return this + */ + fun setHideArmorHeaders(hideArmorHeaders: Boolean) = apply { + _hideArmorHeaders = hideArmorHeaders + } + + companion object { + /** + * Sign and encrypt some data. + * + * @param encryptionOptions encryption options + * @param signingOptions signing options + * @return builder + */ + @JvmStatic + fun signAndEncrypt(encryptionOptions: EncryptionOptions, signingOptions: SigningOptions) = + ProducerOptions(encryptionOptions, signingOptions) + + /** + * Sign some data without encryption. + * + * @param signingOptions signing options + * @return builder + */ + @JvmStatic fun sign(signingOptions: SigningOptions) = ProducerOptions(null, signingOptions) + + /** + * Encrypt some data without signing. + * + * @param encryptionOptions encryption options + * @return builder + */ + @JvmStatic + fun encrypt(encryptionOptions: EncryptionOptions) = ProducerOptions(encryptionOptions, null) + + /** + * Only wrap the data in an OpenPGP packet. No encryption or signing will be applied. + * + * @return builder + */ + @JvmStatic fun noEncryptionNoSigning() = ProducerOptions(null, null) + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/SignatureGenerationStream.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/SignatureGenerationStream.kt new file mode 100644 index 00000000..b2d53dcd --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/SignatureGenerationStream.kt @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.encryption_signing + +import java.io.OutputStream + +/** OutputStream which has the task of updating signature generators for written data. */ +class SignatureGenerationStream( + private val wrapped: OutputStream, + private val options: SigningOptions? +) : OutputStream() { + + override fun close() = wrapped.close() + + override fun flush() = wrapped.flush() + + override fun write(b: Int) { + wrapped.write(b) + options?.run { + signingMethods.values.forEach { it.signatureGenerator.update((b and 0xff).toByte()) } + } + } + + override fun write(b: ByteArray) = write(b, 0, b.size) + + override fun write(b: ByteArray, off: Int, len: Int) { + wrapped.write(b, off, len) + options?.run { signingMethods.values.forEach { it.signatureGenerator.update(b, off, len) } } + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/SigningOptions.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/SigningOptions.kt new file mode 100644 index 00000000..e0fe2972 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/SigningOptions.kt @@ -0,0 +1,483 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.encryption_signing + +import java.util.* +import org.bouncycastle.openpgp.* +import org.pgpainless.PGPainless.Companion.getPolicy +import org.pgpainless.PGPainless.Companion.inspectKeyRing +import org.pgpainless.algorithm.DocumentSignatureType +import org.pgpainless.algorithm.HashAlgorithm +import org.pgpainless.algorithm.PublicKeyAlgorithm.Companion.requireFromId +import org.pgpainless.algorithm.negotiation.HashAlgorithmNegotiator.Companion.negotiateSignatureHashAlgorithm +import org.pgpainless.bouncycastle.extensions.unlock +import org.pgpainless.exception.KeyException +import org.pgpainless.exception.KeyException.* +import org.pgpainless.implementation.ImplementationFactory +import org.pgpainless.key.OpenPgpFingerprint.Companion.of +import org.pgpainless.key.SubkeyIdentifier +import org.pgpainless.key.protection.SecretKeyRingProtector +import org.pgpainless.policy.Policy +import org.pgpainless.signature.subpackets.BaseSignatureSubpackets.Callback +import org.pgpainless.signature.subpackets.SignatureSubpackets +import org.pgpainless.signature.subpackets.SignatureSubpacketsHelper + +class SigningOptions { + + val signingMethods: Map = mutableMapOf() + private var _hashAlgorithmOverride: HashAlgorithm? = null + private var _evaluationDate: Date = Date() + + val hashAlgorithmOverride: HashAlgorithm? + get() = _hashAlgorithmOverride + + /** + * Override hash algorithm negotiation by dictating which hash algorithm needs to be used. If no + * override has been set, an acceptable algorithm will be negotiated instead. Note: To override + * the hash algorithm for signing, call this method *before* calling [addInlineSignature] or + * [addDetachedSignature]. + * + * @param hashAlgorithmOverride override hash algorithm + * @return this + */ + fun overrideHashAlgorithm(hashAlgorithmOverride: HashAlgorithm) = apply { + _hashAlgorithmOverride = hashAlgorithmOverride + } + + val evaluationDate: Date + get() = _evaluationDate + + /** + * Override the evaluation date for signing keys with the given date. + * + * @param evaluationDate new evaluation date + * @return this + */ + fun setEvaluationDate(evaluationDate: Date) = apply { _evaluationDate = evaluationDate } + + /** + * Sign the message using an inline signature made by the provided signing key. + * + * @param signingKeyProtector protector to unlock the signing key + * @param signingKey key ring containing the signing key + * @return this + * @throws KeyException if something is wrong with the key + * @throws PGPException if the key cannot be unlocked or a signing method cannot be created + */ + @Throws(KeyException::class, PGPException::class) + fun addSignature(signingKeyProtector: SecretKeyRingProtector, signingKey: PGPSecretKeyRing) = + apply { + addInlineSignature( + signingKeyProtector, signingKey, null, DocumentSignatureType.BINARY_DOCUMENT) + } + + /** + * Add inline signatures with all secret key rings in the provided secret key ring collection. + * + * @param signingKeyProtector decryptor to unlock the signing secret keys + * @param signingKeys collection of signing keys + * @param signatureType type of signature (binary, canonical text) + * @return this + * @throws KeyException if something is wrong with any of the keys + * @throws PGPException if any of the keys cannot be unlocked or a signing method cannot be + * created + */ + @Throws(KeyException::class, PGPException::class) + fun addInlineSignatures( + signingKeyProtector: SecretKeyRingProtector, + signingKeys: Iterable, + signatureType: DocumentSignatureType + ) = apply { + signingKeys.forEach { addInlineSignature(signingKeyProtector, it, null, signatureType) } + } + + /** + * Add an inline-signature. Inline signatures are being embedded into the message itself and can + * be processed in one pass, thanks to the use of one-pass-signature packets. + * + * @param signingKeyProtector decryptor to unlock the signing secret key + * @param signingKey signing key + * @param signatureType type of signature (binary, canonical text) + * @return this + * @throws KeyException if something is wrong with the key + * @throws PGPException if the key cannot be unlocked or the signing method cannot be created + */ + @Throws(KeyException::class, PGPException::class) + fun addInlineSignature( + signingKeyProtector: SecretKeyRingProtector, + signingKey: PGPSecretKeyRing, + signatureType: DocumentSignatureType + ) = apply { addInlineSignature(signingKeyProtector, signingKey, null, signatureType) } + + /** + * Add an inline-signature. Inline signatures are being embedded into the message itself and can + * be processed in one pass, thanks to the use of one-pass-signature packets. + * + *

+ * This method uses the passed in user-id to select user-specific hash algorithms. + * + * @param signingKeyProtector decryptor to unlock the signing secret key + * @param signingKey signing key + * @param userId user-id of the signer + * @param signatureType signature type (binary, canonical text) + * @param subpacketsCallback callback to modify the hashed and unhashed subpackets of the + * signature + * @return this + * @throws KeyException if the key is invalid + * @throws PGPException if the key cannot be unlocked or the signing method cannot be created + */ + @Throws(KeyException::class, PGPException::class) + @JvmOverloads + fun addInlineSignature( + signingKeyProtector: SecretKeyRingProtector, + signingKey: PGPSecretKeyRing, + userId: CharSequence? = null, + signatureType: DocumentSignatureType = DocumentSignatureType.BINARY_DOCUMENT, + subpacketsCallback: Callback? = null + ) = apply { + val keyRingInfo = inspectKeyRing(signingKey, evaluationDate) + if (userId != null && !keyRingInfo.isUserIdValid(userId)) { + throw UnboundUserIdException( + of(signingKey), + userId.toString(), + keyRingInfo.getLatestUserIdCertification(userId), + keyRingInfo.getUserIdRevocation(userId)) + } + + val signingPubKeys = keyRingInfo.signingSubkeys + if (signingPubKeys.isEmpty()) { + throw UnacceptableSigningKeyException(of(signingKey)) + } + + for (signingPubKey in signingPubKeys) { + val signingSecKey: PGPSecretKey = + signingKey.getSecretKey(signingPubKey.keyID) + ?: throw MissingSecretKeyException(of(signingKey), signingPubKey.keyID) + val signingSubkey: PGPPrivateKey = signingSecKey.unlock(signingKeyProtector) + val hashAlgorithms = + if (userId != null) keyRingInfo.getPreferredHashAlgorithms(userId) + else keyRingInfo.getPreferredHashAlgorithms(signingPubKey.keyID) + val hashAlgorithm: HashAlgorithm = negotiateHashAlgorithm(hashAlgorithms, getPolicy()) + addSigningMethod( + signingKey, signingSubkey, hashAlgorithm, signatureType, false, subpacketsCallback) + } + } + + /** + * Create an inline signature using the signing key with the given keyId. + * + * @param signingKeyProtector decryptor to unlock the secret key + * @param signingKey secret key ring + * @param keyId keyId of the signing (sub-)key + * @param signatureType signature type + * @param subpacketsCallback callback to modify the signatures subpackets + * @return builder + * @throws PGPException if the secret key cannot be unlocked or if no signing method can be + * created. + * @throws KeyException.UnacceptableSigningKeyException if the key ring does not carry any + * signing-capable subkeys + * @throws KeyException.MissingSecretKeyException if the key ring does not contain the + * identified secret key + */ + @Throws(KeyException::class, PGPException::class) + @JvmOverloads + fun addInlineSignature( + signingKeyProtector: SecretKeyRingProtector, + signingKey: PGPSecretKeyRing, + keyId: Long, + signatureType: DocumentSignatureType = DocumentSignatureType.BINARY_DOCUMENT, + subpacketsCallback: Callback? = null + ) = apply { + val keyRingInfo = inspectKeyRing(signingKey, evaluationDate) + val signingPubKeys = keyRingInfo.signingSubkeys + if (signingPubKeys.isEmpty()) { + throw UnacceptableSigningKeyException(of(signingKey)) + } + + for (signingPubKey in signingPubKeys) { + if (signingPubKey.keyID != keyId) { + continue + } + + val signingSecKey = + signingKey.getSecretKey(signingPubKey.keyID) + ?: throw MissingSecretKeyException(of(signingKey), signingPubKey.keyID) + val signingSubkey = signingSecKey.unlock(signingKeyProtector) + val hashAlgorithms = keyRingInfo.getPreferredHashAlgorithms(signingPubKey.keyID) + val hashAlgorithm: HashAlgorithm = negotiateHashAlgorithm(hashAlgorithms, getPolicy()) + addSigningMethod( + signingKey, signingSubkey, hashAlgorithm, signatureType, false, subpacketsCallback) + return this + } + throw MissingSecretKeyException(of(signingKey), keyId) + } + + /** + * Add detached signatures with all key rings from the provided secret key ring collection. + * + * @param signingKeyProtector decryptor to unlock the secret signing keys + * @param signingKeys collection of signing key rings + * @param signatureType type of the signature (binary, canonical text) + * @return this + * @throws KeyException if something is wrong with any of the keys + * @throws PGPException if any of the keys cannot be validated or unlocked, or if any signing + * method cannot be created + */ + @Throws(KeyException::class, PGPException::class) + fun addDetachedSignatures( + signingKeyProtector: SecretKeyRingProtector, + signingKeys: Iterable, + signatureType: DocumentSignatureType + ) = apply { + signingKeys.forEach { addDetachedSignature(signingKeyProtector, it, null, signatureType) } + } + + /** + * Create a detached signature. Detached signatures are not being added into the PGP message + * itself. Instead, they can be distributed separately to the message. Detached signatures are + * useful if the data that is being signed shall not be modified (e.g. when signing a file). + * + * @param signingKeyProtector decryptor to unlock the secret signing key + * @param signingKey signing key + * @param signatureType type of data that is signed (binary, canonical text) + * @return this + * @throws KeyException if something is wrong with the key + * @throws PGPException if the key cannot be validated or unlocked, or if no signature method + * can be created + */ + @Throws(KeyException::class, PGPException::class) + fun addDetachedSignature( + signingKeyProtector: SecretKeyRingProtector, + signingKey: PGPSecretKeyRing, + signatureType: DocumentSignatureType + ) = apply { addDetachedSignature(signingKeyProtector, signingKey, null, signatureType) } + + /** + * Create a detached signature. Detached signatures are not being added into the PGP message + * itself. Instead, they can be distributed separately to the message. Detached signatures are + * useful if the data that is being signed shall not be modified (e.g. when signing a file). + * + *

+ * This method uses the passed in user-id to select user-specific hash algorithms. + * + * @param signingKeyProtector decryptor to unlock the secret signing key + * @param signingKey signing key + * @param userId user-id + * @param signatureType type of data that is signed (binary, canonical text) + * @param subpacketCallback callback to modify hashed and unhashed subpackets of the signature + * @return this + * @throws KeyException if something is wrong with the key + * @throws PGPException if the key cannot be validated or unlocked, or if no signature method + * can be created + */ + @JvmOverloads + @Throws(KeyException::class, PGPException::class) + fun addDetachedSignature( + signingKeyProtector: SecretKeyRingProtector, + signingKey: PGPSecretKeyRing, + userId: String? = null, + signatureType: DocumentSignatureType = DocumentSignatureType.BINARY_DOCUMENT, + subpacketCallback: Callback? = null + ) = apply { + val keyRingInfo = inspectKeyRing(signingKey, evaluationDate) + if (userId != null && !keyRingInfo.isUserIdValid(userId)) { + throw UnboundUserIdException( + of(signingKey), + userId.toString(), + keyRingInfo.getLatestUserIdCertification(userId), + keyRingInfo.getUserIdRevocation(userId)) + } + + val signingPubKeys = keyRingInfo.signingSubkeys + if (signingPubKeys.isEmpty()) { + throw UnacceptableSigningKeyException(of(signingKey)) + } + + for (signingPubKey in signingPubKeys) { + val signingSecKey: PGPSecretKey = + signingKey.getSecretKey(signingPubKey.keyID) + ?: throw MissingSecretKeyException(of(signingKey), signingPubKey.keyID) + val signingSubkey: PGPPrivateKey = signingSecKey.unlock(signingKeyProtector) + val hashAlgorithms = + if (userId != null) keyRingInfo.getPreferredHashAlgorithms(userId) + else keyRingInfo.getPreferredHashAlgorithms(signingPubKey.keyID) + val hashAlgorithm: HashAlgorithm = negotiateHashAlgorithm(hashAlgorithms, getPolicy()) + addSigningMethod( + signingKey, signingSubkey, hashAlgorithm, signatureType, true, subpacketCallback) + } + } + + /** + * Create a detached signature using the signing key with the given keyId. + * + * @param signingKeyProtector decryptor to unlock the secret key + * @param signingKey secret key ring + * @param keyId keyId of the signing (sub-)key + * @param signatureType signature type + * @param subpacketsCallback callback to modify the signatures subpackets + * @return builder + * @throws PGPException if the secret key cannot be unlocked or if no signing method can be + * created. + * @throws KeyException.UnacceptableSigningKeyException if the key ring does not carry any + * signing-capable subkeys + * @throws KeyException.MissingSecretKeyException if the key ring does not contain the + * identified secret key + */ + @Throws(KeyException::class, PGPException::class) + @JvmOverloads + fun addDetachedSignature( + signingKeyProtector: SecretKeyRingProtector, + signingKey: PGPSecretKeyRing, + keyId: Long, + signatureType: DocumentSignatureType = DocumentSignatureType.BINARY_DOCUMENT, + subpacketsCallback: Callback? = null + ) = apply { + val keyRingInfo = inspectKeyRing(signingKey, evaluationDate) + + val signingPubKeys = keyRingInfo.signingSubkeys + if (signingPubKeys.isEmpty()) { + throw UnacceptableSigningKeyException(of(signingKey)) + } + + for (signingPubKey in signingPubKeys) { + if (signingPubKey.keyID == keyId) { + val signingSecKey: PGPSecretKey = + signingKey.getSecretKey(signingPubKey.keyID) + ?: throw MissingSecretKeyException(of(signingKey), signingPubKey.keyID) + val signingSubkey: PGPPrivateKey = signingSecKey.unlock(signingKeyProtector) + val hashAlgorithms = keyRingInfo.getPreferredHashAlgorithms(signingPubKey.keyID) + val hashAlgorithm: HashAlgorithm = + negotiateHashAlgorithm(hashAlgorithms, getPolicy()) + addSigningMethod( + signingKey, + signingSubkey, + hashAlgorithm, + signatureType, + true, + subpacketsCallback) + return this + } + } + + throw MissingSecretKeyException(of(signingKey), keyId) + } + + private fun addSigningMethod( + signingKey: PGPSecretKeyRing, + signingSubkey: PGPPrivateKey, + hashAlgorithm: HashAlgorithm, + signatureType: DocumentSignatureType, + detached: Boolean, + subpacketCallback: Callback? = null + ) { + val signingKeyIdentifier = SubkeyIdentifier(signingKey, signingSubkey.keyID) + val signingSecretKey: PGPSecretKey = signingKey.getSecretKey(signingSubkey.keyID) + val publicKeyAlgorithm = requireFromId(signingSecretKey.publicKey.algorithm) + val bitStrength = signingSecretKey.publicKey.bitStrength + if (!getPolicy().publicKeyAlgorithmPolicy.isAcceptable(publicKeyAlgorithm, bitStrength)) { + throw UnacceptableSigningKeyException( + PublicKeyAlgorithmPolicyException( + of(signingKey), signingSecretKey.keyID, publicKeyAlgorithm, bitStrength)) + } + + val generator: PGPSignatureGenerator = + createSignatureGenerator(signingSubkey, hashAlgorithm, signatureType) + + // Subpackets + val hashedSubpackets = + SignatureSubpackets.createHashedSubpackets(signingSecretKey.publicKey) + val unhashedSubpackets = SignatureSubpackets.createEmptySubpackets() + if (subpacketCallback != null) { + subpacketCallback.modifyHashedSubpackets(hashedSubpackets) + subpacketCallback.modifyUnhashedSubpackets(unhashedSubpackets) + } + generator.setHashedSubpackets(SignatureSubpacketsHelper.toVector(hashedSubpackets)) + generator.setUnhashedSubpackets(SignatureSubpacketsHelper.toVector(unhashedSubpackets)) + + val signingMethod = + if (detached) SigningMethod.detachedSignature(generator, hashAlgorithm) + else SigningMethod.inlineSignature(generator, hashAlgorithm) + (signingMethods as MutableMap)[signingKeyIdentifier] = signingMethod + } + + /** + * Negotiate, which hash algorithm to use. + * + * This method gives the highest priority to the algorithm override, which can be set via + * [.overrideHashAlgorithm]. After that, the signing keys hash algorithm preferences are + * iterated to find the first acceptable algorithm. Lastly, should no acceptable algorithm be + * found, the [Policies][Policy] default signature hash algorithm is used as a fallback. + * + * @param preferences preferences + * @param policy policy + * @return selected hash algorithm + */ + private fun negotiateHashAlgorithm( + preferences: Set, + policy: Policy + ): HashAlgorithm { + return _hashAlgorithmOverride + ?: negotiateSignatureHashAlgorithm(policy).negotiateHashAlgorithm(preferences) + } + + @Throws(PGPException::class) + private fun createSignatureGenerator( + privateKey: PGPPrivateKey, + hashAlgorithm: HashAlgorithm, + signatureType: DocumentSignatureType + ): PGPSignatureGenerator { + return ImplementationFactory.getInstance() + .getPGPContentSignerBuilder( + privateKey.publicKeyPacket.algorithm, hashAlgorithm.algorithmId) + .let { csb -> + PGPSignatureGenerator(csb).also { + it.init(signatureType.signatureType.code, privateKey) + } + } + } + + companion object { + @JvmStatic fun get() = SigningOptions() + } + + /** A method of signing. */ + class SigningMethod + private constructor( + val signatureGenerator: PGPSignatureGenerator, + val isDetached: Boolean, + val hashAlgorithm: HashAlgorithm + ) { + companion object { + + /** + * Inline-signature method. The resulting signature will be written into the message + * itself, together with a one-pass-signature packet. + * + * @param signatureGenerator signature generator + * @param hashAlgorithm hash algorithm used to generate the signature + * @return inline signing method + */ + @JvmStatic + fun inlineSignature( + signatureGenerator: PGPSignatureGenerator, + hashAlgorithm: HashAlgorithm + ) = SigningMethod(signatureGenerator, false, hashAlgorithm) + + /** + * Detached signing method. The resulting signature will not be added to the message, + * and instead can be distributed separately to the signed message. + * + * @param signatureGenerator signature generator + * @param hashAlgorithm hash algorithm used to generate the signature + * @return detached signing method + */ + @JvmStatic + fun detachedSignature( + signatureGenerator: PGPSignatureGenerator, + hashAlgorithm: HashAlgorithm + ) = SigningMethod(signatureGenerator, true, hashAlgorithm) + } + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/implementation/BcImplementationFactory.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/implementation/BcImplementationFactory.kt new file mode 100644 index 00000000..dcf594ea --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/implementation/BcImplementationFactory.kt @@ -0,0 +1,114 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.implementation + +import java.io.InputStream +import java.security.KeyPair +import java.util.* +import org.bouncycastle.crypto.AsymmetricCipherKeyPair +import org.bouncycastle.openpgp.* +import org.bouncycastle.openpgp.bc.BcPGPObjectFactory +import org.bouncycastle.openpgp.operator.* +import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator +import org.bouncycastle.openpgp.operator.bc.BcPBEDataDecryptorFactory +import org.bouncycastle.openpgp.operator.bc.BcPBEKeyEncryptionMethodGenerator +import org.bouncycastle.openpgp.operator.bc.BcPBESecretKeyDecryptorBuilder +import org.bouncycastle.openpgp.operator.bc.BcPBESecretKeyEncryptorBuilder +import org.bouncycastle.openpgp.operator.bc.BcPGPContentSignerBuilder +import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider +import org.bouncycastle.openpgp.operator.bc.BcPGPDataEncryptorBuilder +import org.bouncycastle.openpgp.operator.bc.BcPGPDigestCalculatorProvider +import org.bouncycastle.openpgp.operator.bc.BcPGPKeyConverter +import org.bouncycastle.openpgp.operator.bc.BcPGPKeyPair +import org.bouncycastle.openpgp.operator.bc.BcPublicKeyDataDecryptorFactory +import org.bouncycastle.openpgp.operator.bc.BcPublicKeyKeyEncryptionMethodGenerator +import org.bouncycastle.openpgp.operator.bc.BcSessionKeyDataDecryptorFactory +import org.bouncycastle.openpgp.operator.jcajce.JcaPGPKeyPair +import org.pgpainless.algorithm.HashAlgorithm +import org.pgpainless.algorithm.PublicKeyAlgorithm +import org.pgpainless.algorithm.SymmetricKeyAlgorithm +import org.pgpainless.util.Passphrase + +class BcImplementationFactory : ImplementationFactory() { + override val pgpDigestCalculatorProvider: BcPGPDigestCalculatorProvider = + BcPGPDigestCalculatorProvider() + override val pgpContentVerifierBuilderProvider: BcPGPContentVerifierBuilderProvider = + BcPGPContentVerifierBuilderProvider() + override val keyFingerprintCalculator: BcKeyFingerprintCalculator = BcKeyFingerprintCalculator() + + override fun getPBESecretKeyEncryptor( + symmetricKeyAlgorithm: SymmetricKeyAlgorithm, + digestCalculator: PGPDigestCalculator, + passphrase: Passphrase + ): PBESecretKeyEncryptor = + BcPBESecretKeyEncryptorBuilder(symmetricKeyAlgorithm.algorithmId, digestCalculator) + .build(passphrase.getChars()) + + override fun getPBESecretKeyEncryptor( + encryptionAlgorithm: SymmetricKeyAlgorithm, + hashAlgorithm: HashAlgorithm, + s2kCount: Int, + passphrase: Passphrase + ): PBESecretKeyEncryptor = + BcPBESecretKeyEncryptorBuilder( + encryptionAlgorithm.algorithmId, getPGPDigestCalculator(hashAlgorithm), s2kCount) + .build(passphrase.getChars()) + + override fun getPBESecretKeyDecryptor(passphrase: Passphrase): PBESecretKeyDecryptor = + BcPBESecretKeyDecryptorBuilder(pgpDigestCalculatorProvider).build(passphrase.getChars()) + + override fun getPGPContentSignerBuilder( + keyAlgorithm: Int, + hashAlgorithm: Int + ): PGPContentSignerBuilder = BcPGPContentSignerBuilder(keyAlgorithm, hashAlgorithm) + + override fun getPBEDataDecryptorFactory(passphrase: Passphrase): PBEDataDecryptorFactory = + BcPBEDataDecryptorFactory(passphrase.getChars(), pgpDigestCalculatorProvider) + + override fun getPublicKeyDataDecryptorFactory( + privateKey: PGPPrivateKey + ): PublicKeyDataDecryptorFactory = BcPublicKeyDataDecryptorFactory(privateKey) + + override fun getSessionKeyDataDecryptorFactory( + sessionKey: PGPSessionKey + ): SessionKeyDataDecryptorFactory = BcSessionKeyDataDecryptorFactory(sessionKey) + + override fun getPublicKeyKeyEncryptionMethodGenerator( + key: PGPPublicKey + ): PublicKeyKeyEncryptionMethodGenerator = BcPublicKeyKeyEncryptionMethodGenerator(key) + + override fun getPBEKeyEncryptionMethodGenerator( + passphrase: Passphrase + ): PBEKeyEncryptionMethodGenerator = BcPBEKeyEncryptionMethodGenerator(passphrase.getChars()) + + override fun getPGPDataEncryptorBuilder(symmetricKeyAlgorithm: Int): PGPDataEncryptorBuilder = + BcPGPDataEncryptorBuilder(symmetricKeyAlgorithm) + + override fun getPGPKeyPair( + publicKeyAlgorithm: PublicKeyAlgorithm, + keyPair: KeyPair, + creationDate: Date + ): PGPKeyPair = + BcPGPKeyPair( + publicKeyAlgorithm.algorithmId, + jceToBcKeyPair(publicKeyAlgorithm, keyPair, creationDate), + creationDate) + + override fun getPGPObjectFactory(inputStream: InputStream): PGPObjectFactory = + BcPGPObjectFactory(inputStream) + + private fun jceToBcKeyPair( + publicKeyAlgorithm: PublicKeyAlgorithm, + keyPair: KeyPair, + creationDate: Date + ): AsymmetricCipherKeyPair = + BcPGPKeyConverter().let { converter -> + JcaPGPKeyPair(publicKeyAlgorithm.algorithmId, keyPair, creationDate).let { pair -> + AsymmetricCipherKeyPair( + converter.getPublicKey(pair.publicKey), + converter.getPrivateKey(pair.privateKey)) + } + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/implementation/ImplementationFactory.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/implementation/ImplementationFactory.kt new file mode 100644 index 00000000..58478379 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/implementation/ImplementationFactory.kt @@ -0,0 +1,117 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.implementation + +import java.io.InputStream +import java.security.KeyPair +import java.util.* +import org.bouncycastle.openpgp.* +import org.bouncycastle.openpgp.operator.* +import org.pgpainless.algorithm.HashAlgorithm +import org.pgpainless.algorithm.PublicKeyAlgorithm +import org.pgpainless.algorithm.SymmetricKeyAlgorithm +import org.pgpainless.util.Passphrase +import org.pgpainless.util.SessionKey + +abstract class ImplementationFactory { + + companion object { + @JvmStatic private var instance: ImplementationFactory = BcImplementationFactory() + + @JvmStatic fun getInstance() = instance + + @JvmStatic + fun setFactoryImplementation(implementation: ImplementationFactory) = apply { + instance = implementation + } + } + + abstract val pgpDigestCalculatorProvider: PGPDigestCalculatorProvider + abstract val pgpContentVerifierBuilderProvider: PGPContentVerifierBuilderProvider + abstract val keyFingerprintCalculator: KeyFingerPrintCalculator + + val v4FingerprintCalculator: PGPDigestCalculator + get() = getPGPDigestCalculator(HashAlgorithm.SHA1) + + @Throws(PGPException::class) + abstract fun getPBESecretKeyEncryptor( + symmetricKeyAlgorithm: SymmetricKeyAlgorithm, + digestCalculator: PGPDigestCalculator, + passphrase: Passphrase + ): PBESecretKeyEncryptor + + @Throws(PGPException::class) + abstract fun getPBESecretKeyDecryptor(passphrase: Passphrase): PBESecretKeyDecryptor + + @Throws(PGPException::class) + abstract fun getPBESecretKeyEncryptor( + encryptionAlgorithm: SymmetricKeyAlgorithm, + hashAlgorithm: HashAlgorithm, + s2kCount: Int, + passphrase: Passphrase + ): PBESecretKeyEncryptor + + fun getPGPDigestCalculator(hashAlgorithm: HashAlgorithm): PGPDigestCalculator = + getPGPDigestCalculator(hashAlgorithm.algorithmId) + + fun getPGPDigestCalculator(hashAlgorithm: Int): PGPDigestCalculator = + pgpDigestCalculatorProvider.get(hashAlgorithm) + + fun getPGPContentSignerBuilder( + keyAlgorithm: PublicKeyAlgorithm, + hashAlgorithm: HashAlgorithm + ): PGPContentSignerBuilder = + getPGPContentSignerBuilder(keyAlgorithm.algorithmId, hashAlgorithm.algorithmId) + + abstract fun getPGPContentSignerBuilder( + keyAlgorithm: Int, + hashAlgorithm: Int + ): PGPContentSignerBuilder + + @Throws(PGPException::class) + abstract fun getPBEDataDecryptorFactory(passphrase: Passphrase): PBEDataDecryptorFactory + + abstract fun getPublicKeyDataDecryptorFactory( + privateKey: PGPPrivateKey + ): PublicKeyDataDecryptorFactory + + fun getSessionKeyDataDecryptorFactory(sessionKey: SessionKey): SessionKeyDataDecryptorFactory = + getSessionKeyDataDecryptorFactory( + PGPSessionKey(sessionKey.algorithm.algorithmId, sessionKey.key)) + + abstract fun getSessionKeyDataDecryptorFactory( + sessionKey: PGPSessionKey + ): SessionKeyDataDecryptorFactory + + abstract fun getPublicKeyKeyEncryptionMethodGenerator( + key: PGPPublicKey + ): PublicKeyKeyEncryptionMethodGenerator + + abstract fun getPBEKeyEncryptionMethodGenerator( + passphrase: Passphrase + ): PBEKeyEncryptionMethodGenerator + + fun getPGPDataEncryptorBuilder( + symmetricKeyAlgorithm: SymmetricKeyAlgorithm + ): PGPDataEncryptorBuilder = getPGPDataEncryptorBuilder(symmetricKeyAlgorithm.algorithmId) + + abstract fun getPGPDataEncryptorBuilder(symmetricKeyAlgorithm: Int): PGPDataEncryptorBuilder + + @Throws(PGPException::class) + abstract fun getPGPKeyPair( + publicKeyAlgorithm: PublicKeyAlgorithm, + keyPair: KeyPair, + creationDate: Date + ): PGPKeyPair + + fun getPGPObjectFactory(bytes: ByteArray): PGPObjectFactory = + getPGPObjectFactory(bytes.inputStream()) + + abstract fun getPGPObjectFactory(inputStream: InputStream): PGPObjectFactory + + override fun toString(): String { + return javaClass.simpleName + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/implementation/JceImplementationFactory.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/implementation/JceImplementationFactory.kt new file mode 100644 index 00000000..865f1e0d --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/implementation/JceImplementationFactory.kt @@ -0,0 +1,112 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.implementation + +import java.io.InputStream +import java.security.KeyPair +import java.util.* +import org.bouncycastle.openpgp.* +import org.bouncycastle.openpgp.operator.* +import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator +import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder +import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider +import org.bouncycastle.openpgp.operator.jcajce.JcaPGPDigestCalculatorProviderBuilder +import org.bouncycastle.openpgp.operator.jcajce.JcaPGPKeyPair +import org.bouncycastle.openpgp.operator.jcajce.JcePBEDataDecryptorFactoryBuilder +import org.bouncycastle.openpgp.operator.jcajce.JcePBEKeyEncryptionMethodGenerator +import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder +import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyEncryptorBuilder +import org.bouncycastle.openpgp.operator.jcajce.JcePGPDataEncryptorBuilder +import org.bouncycastle.openpgp.operator.jcajce.JcePublicKeyDataDecryptorFactoryBuilder +import org.bouncycastle.openpgp.operator.jcajce.JcePublicKeyKeyEncryptionMethodGenerator +import org.bouncycastle.openpgp.operator.jcajce.JceSessionKeyDataDecryptorFactoryBuilder +import org.pgpainless.algorithm.HashAlgorithm +import org.pgpainless.algorithm.PublicKeyAlgorithm +import org.pgpainless.algorithm.SymmetricKeyAlgorithm +import org.pgpainless.provider.ProviderFactory +import org.pgpainless.util.Passphrase + +class JceImplementationFactory : ImplementationFactory() { + override val pgpDigestCalculatorProvider: PGPDigestCalculatorProvider = + JcaPGPDigestCalculatorProviderBuilder().setProvider(ProviderFactory.provider).build() + override val pgpContentVerifierBuilderProvider: PGPContentVerifierBuilderProvider = + JcaPGPContentVerifierBuilderProvider().setProvider(ProviderFactory.provider) + override val keyFingerprintCalculator: KeyFingerPrintCalculator = + JcaKeyFingerprintCalculator().setProvider(ProviderFactory.provider) + + override fun getPBESecretKeyEncryptor( + symmetricKeyAlgorithm: SymmetricKeyAlgorithm, + digestCalculator: PGPDigestCalculator, + passphrase: Passphrase + ): PBESecretKeyEncryptor = + JcePBESecretKeyEncryptorBuilder(symmetricKeyAlgorithm.algorithmId, digestCalculator) + .setProvider(ProviderFactory.provider) + .build(passphrase.getChars()) + + override fun getPBESecretKeyEncryptor( + encryptionAlgorithm: SymmetricKeyAlgorithm, + hashAlgorithm: HashAlgorithm, + s2kCount: Int, + passphrase: Passphrase + ): PBESecretKeyEncryptor = + JcePBESecretKeyEncryptorBuilder( + encryptionAlgorithm.algorithmId, getPGPDigestCalculator(hashAlgorithm), s2kCount) + .setProvider(ProviderFactory.provider) + .build(passphrase.getChars()) + + override fun getPBESecretKeyDecryptor(passphrase: Passphrase): PBESecretKeyDecryptor = + JcePBESecretKeyDecryptorBuilder(pgpDigestCalculatorProvider) + .setProvider(ProviderFactory.provider) + .build(passphrase.getChars()) + + override fun getPGPContentSignerBuilder( + keyAlgorithm: Int, + hashAlgorithm: Int + ): PGPContentSignerBuilder = + JcaPGPContentSignerBuilder(keyAlgorithm, hashAlgorithm) + .setProvider(ProviderFactory.provider) + + override fun getPBEDataDecryptorFactory(passphrase: Passphrase): PBEDataDecryptorFactory = + JcePBEDataDecryptorFactoryBuilder(pgpDigestCalculatorProvider) + .setProvider(ProviderFactory.provider) + .build(passphrase.getChars()) + + override fun getPublicKeyDataDecryptorFactory( + privateKey: PGPPrivateKey + ): PublicKeyDataDecryptorFactory = + JcePublicKeyDataDecryptorFactoryBuilder() + .setProvider(ProviderFactory.provider) + .build(privateKey) + + override fun getSessionKeyDataDecryptorFactory( + sessionKey: PGPSessionKey + ): SessionKeyDataDecryptorFactory = + JceSessionKeyDataDecryptorFactoryBuilder() + .setProvider(ProviderFactory.provider) + .build(sessionKey) + + override fun getPublicKeyKeyEncryptionMethodGenerator( + key: PGPPublicKey + ): PublicKeyKeyEncryptionMethodGenerator = + JcePublicKeyKeyEncryptionMethodGenerator(key).setProvider(ProviderFactory.provider) + + override fun getPBEKeyEncryptionMethodGenerator( + passphrase: Passphrase + ): PBEKeyEncryptionMethodGenerator = + JcePBEKeyEncryptionMethodGenerator(passphrase.getChars()) + .setProvider(ProviderFactory.provider) + + override fun getPGPDataEncryptorBuilder(symmetricKeyAlgorithm: Int): PGPDataEncryptorBuilder = + JcePGPDataEncryptorBuilder(symmetricKeyAlgorithm).setProvider(ProviderFactory.provider) + + override fun getPGPKeyPair( + publicKeyAlgorithm: PublicKeyAlgorithm, + keyPair: KeyPair, + creationDate: Date + ): PGPKeyPair = JcaPGPKeyPair(publicKeyAlgorithm.algorithmId, keyPair, creationDate) + + override fun getPGPObjectFactory(inputStream: InputStream): PGPObjectFactory = + PGPObjectFactory(inputStream, keyFingerprintCalculator) +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/OpenPgpFingerprint.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/OpenPgpFingerprint.kt new file mode 100644 index 00000000..9a2f1f7b --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/OpenPgpFingerprint.kt @@ -0,0 +1,166 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key + +import java.nio.charset.Charset +import org.bouncycastle.openpgp.PGPKeyRing +import org.bouncycastle.openpgp.PGPPublicKey +import org.bouncycastle.openpgp.PGPSecretKey +import org.bouncycastle.util.encoders.Hex + +/** Abstract super class of different version OpenPGP fingerprints. */ +abstract class OpenPgpFingerprint : CharSequence, Comparable { + val fingerprint: String + val bytes: ByteArray + + /** + * Return the version of the fingerprint. + * + * @return version + */ + abstract fun getVersion(): Int + + /** + * Return the key id of the OpenPGP public key this [OpenPgpFingerprint] belongs to. This method + * can be implemented for V4 and V5 fingerprints. V3 key-IDs cannot be derived from the + * fingerprint, but we don't care, since V3 is deprecated. + * + * @return key id + * @see RFC-4880 §12.2: Key IDs and + * Fingerprints + */ + abstract val keyId: Long + + constructor(fingerprint: String) { + val prep = fingerprint.replace(" ", "").trim().uppercase() + if (!isValid(prep)) { + throw IllegalArgumentException( + "Fingerprint '$fingerprint' does not appear to be a valid OpenPGP V${getVersion()} fingerprint.") + } + this.fingerprint = prep + this.bytes = Hex.decode(prep) + } + + constructor(bytes: ByteArray) : this(Hex.toHexString(bytes)) + + constructor(key: PGPPublicKey) : this(key.fingerprint) { + if (key.version != getVersion()) { + throw IllegalArgumentException("Key is not a v${getVersion()} OpenPgp key.") + } + } + + constructor(key: PGPSecretKey) : this(key.publicKey) + + constructor(keys: PGPKeyRing) : this(keys.publicKey) + + /** + * Check, whether the fingerprint consists of 40 valid hexadecimal characters. + * + * @param fp fingerprint to check. + * @return true if fingerprint is valid. + */ + protected abstract fun isValid(fingerprint: String): Boolean + + override val length: Int + get() = fingerprint.length + + override fun get(index: Int) = fingerprint.get(index) + + override fun subSequence(startIndex: Int, endIndex: Int) = + fingerprint.subSequence(startIndex, endIndex) + + override fun compareTo(other: OpenPgpFingerprint): Int { + return fingerprint.compareTo(other.fingerprint) + } + + override fun equals(other: Any?): Boolean { + return toString() == other.toString() + } + + override fun hashCode(): Int { + return toString().hashCode() + } + + override fun toString(): String = fingerprint + + abstract fun prettyPrint(): String + + companion object { + @JvmStatic val utf8: Charset = Charset.forName("UTF-8") + + /** + * Return the fingerprint of the given key. This method automatically matches key versions + * to fingerprint implementations. + * + * @param key key + * @return fingerprint + */ + @JvmStatic fun of(key: PGPSecretKey): OpenPgpFingerprint = of(key.publicKey) + + /** + * Return the fingerprint of the given key. This method automatically matches key versions + * to fingerprint implementations. + * + * @param key key + * @return fingerprint + */ + @JvmStatic + fun of(key: PGPPublicKey): OpenPgpFingerprint = + when (key.version) { + 4 -> OpenPgpV4Fingerprint(key) + 5 -> OpenPgpV5Fingerprint(key) + 6 -> OpenPgpV6Fingerprint(key) + else -> + throw IllegalArgumentException( + "OpenPGP keys of version ${key.version} are not supported.") + } + + /** + * Return the fingerprint of the primary key of the given key ring. This method + * automatically matches key versions to fingerprint implementations. + * + * @param ring key ring + * @return fingerprint + */ + @JvmStatic fun of(keys: PGPKeyRing): OpenPgpFingerprint = of(keys.publicKey) + + /** + * Try to parse an [OpenPgpFingerprint] from the given fingerprint string. If the trimmed + * fingerprint without whitespace is 64 characters long, it is either a v5 or v6 + * fingerprint. In this case, we return a [_64DigitFingerprint]. Since this is ambiguous, it + * is generally recommended to know the version of the key beforehand. + * + * @param fingerprint fingerprint + * @return parsed fingerprint + * @deprecated Use the constructor methods of the versioned fingerprint subclasses instead. + */ + @JvmStatic + @Deprecated("Use the constructor methods of the versioned fingerprint subclasses instead.") + fun parse(fingerprint: String): OpenPgpFingerprint { + val prep = fingerprint.replace(" ", "").trim().uppercase() + if (prep.matches("^[0-9A-F]{40}$".toRegex())) { + return OpenPgpV4Fingerprint(prep) + } + if (prep.matches("^[0-9A-F]{64}$".toRegex())) { + // Might be v5 or v6 :/ + return _64DigitFingerprint(prep) + } + throw IllegalArgumentException( + "Fingerprint does not appear to match any known fingerprint pattern.") + } + + /** + * Parse a binary OpenPGP fingerprint into an [OpenPgpFingerprint] object. + * + * @param binaryFingerprint binary representation of the fingerprint + * @return parsed fingerprint + * @deprecated use the parse() methods of the versioned fingerprint subclasses instead. + */ + @JvmStatic + @Deprecated("use the parse() methods of the versioned fingerprint subclasses instead.") + fun parseFromBinary(binaryFingerprint: ByteArray): OpenPgpFingerprint = + parse(Hex.toHexString(binaryFingerprint)) + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/OpenPgpV4Fingerprint.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/OpenPgpV4Fingerprint.kt new file mode 100644 index 00000000..e02f0ae7 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/OpenPgpV4Fingerprint.kt @@ -0,0 +1,72 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key + +import java.net.URI +import java.nio.Buffer +import java.nio.ByteBuffer +import java.nio.charset.Charset +import org.bouncycastle.openpgp.PGPKeyRing +import org.bouncycastle.openpgp.PGPPublicKey +import org.bouncycastle.openpgp.PGPSecretKey +import org.bouncycastle.util.encoders.Hex + +class OpenPgpV4Fingerprint : OpenPgpFingerprint { + + constructor(fingerprint: String) : super(fingerprint) + + constructor(bytes: ByteArray) : super(bytes) + + constructor(key: PGPPublicKey) : super(key) + + constructor(key: PGPSecretKey) : super(key) + + constructor(keys: PGPKeyRing) : super(keys) + + override fun getVersion() = 4 + + override val keyId: Long + get() { + val bytes = Hex.decode(toString().toByteArray(Charset.forName("UTF-8"))) + val buf = ByteBuffer.wrap(bytes) + + // The key id is the right-most 8 bytes (conveniently a long) + // We have to cast here in order to be compatible with java 8 + // https://github.com/eclipse/jetty.project/issues/3244 + (buf as Buffer).position(12) // 20 - 8 bytes = offset 12 + return buf.getLong() + } + + override fun isValid(fingerprint: String): Boolean { + return fingerprint.matches("^[0-9A-F]{40}$".toRegex()) + } + + fun toUri(): URI = URI(SCHEME, toString(), null) + + override fun prettyPrint(): String { + return buildString { + for (i in 0..4) { + append(fingerprint, i * 4, (i + 1) * 4).append(' ') + } + append(' ') + for (i in 5..8) { + append(fingerprint, i * 4, (i + 1) * 4).append(' ') + } + append(fingerprint, 36, 40) + } + } + + companion object { + @JvmStatic val SCHEME = "openpgp4fpr" + + @JvmStatic + fun fromUri(uri: URI): OpenPgpV4Fingerprint { + if (SCHEME != uri.scheme) { + throw IllegalArgumentException("URI scheme MUST equal '$SCHEME'.") + } + return OpenPgpV4Fingerprint(uri.schemeSpecificPart) + } + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/OpenPgpV5Fingerprint.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/OpenPgpV5Fingerprint.kt new file mode 100644 index 00000000..7bc36cc9 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/OpenPgpV5Fingerprint.kt @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key + +import org.bouncycastle.openpgp.PGPKeyRing +import org.bouncycastle.openpgp.PGPPublicKey +import org.bouncycastle.openpgp.PGPSecretKey + +/** This class represents a hex encoded uppercase OpenPGP v5 fingerprint. */ +class OpenPgpV5Fingerprint : _64DigitFingerprint { + + constructor(fingerprint: String) : super(fingerprint) + + constructor(key: PGPPublicKey) : super(key) + + constructor(key: PGPSecretKey) : super(key) + + constructor(keys: PGPKeyRing) : super(keys) + + constructor(bytes: ByteArray) : super(bytes) + + override fun getVersion(): Int { + return 5 + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/OpenPgpV6Fingerprint.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/OpenPgpV6Fingerprint.kt new file mode 100644 index 00000000..0b843ed2 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/OpenPgpV6Fingerprint.kt @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key + +import org.bouncycastle.openpgp.PGPKeyRing +import org.bouncycastle.openpgp.PGPPublicKey +import org.bouncycastle.openpgp.PGPSecretKey + +/** This class represents a hex encoded, uppercase OpenPGP v6 fingerprint. */ +class OpenPgpV6Fingerprint : _64DigitFingerprint { + + constructor(fingerprint: String) : super(fingerprint) + + constructor(key: PGPPublicKey) : super(key) + + constructor(key: PGPSecretKey) : super(key) + + constructor(keys: PGPKeyRing) : super(keys) + + constructor(bytes: ByteArray) : super(bytes) + + override fun getVersion(): Int { + return 6 + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/SubkeyIdentifier.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/SubkeyIdentifier.kt new file mode 100644 index 00000000..2aec7976 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/SubkeyIdentifier.kt @@ -0,0 +1,72 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key + +import openpgp.openPgpKeyId +import org.bouncycastle.openpgp.PGPKeyRing +import org.bouncycastle.openpgp.PGPPublicKey + +/** + * Tuple class used to identify a subkey by fingerprints of the primary key of the subkeys key ring, + * as well as the subkeys fingerprint. + */ +class SubkeyIdentifier( + val primaryKeyFingerprint: OpenPgpFingerprint, + val subkeyFingerprint: OpenPgpFingerprint +) { + + constructor(fingerprint: OpenPgpFingerprint) : this(fingerprint, fingerprint) + + constructor(keys: PGPKeyRing) : this(keys.publicKey) + + constructor(key: PGPPublicKey) : this(OpenPgpFingerprint.of(key)) + + constructor( + keys: PGPKeyRing, + keyId: Long + ) : this( + OpenPgpFingerprint.of(keys.publicKey), + OpenPgpFingerprint.of( + keys.getPublicKey(keyId) + ?: throw NoSuchElementException( + "OpenPGP key does not contain subkey ${keyId.openPgpKeyId()}"))) + + constructor( + keys: PGPKeyRing, + subkeyFingerprint: OpenPgpFingerprint + ) : this(OpenPgpFingerprint.of(keys), subkeyFingerprint) + + val keyId = subkeyFingerprint.keyId + val fingerprint = subkeyFingerprint + + val subkeyId = subkeyFingerprint.keyId + val primaryKeyId = primaryKeyFingerprint.keyId + + val isPrimaryKey = primaryKeyId == subkeyId + + fun matches(fingerprint: OpenPgpFingerprint) = + primaryKeyFingerprint == fingerprint || subkeyFingerprint == fingerprint + + override fun equals(other: Any?): Boolean { + if (other == null) { + return false + } + if (this === other) { + return true + } + if (other !is SubkeyIdentifier) { + return false + } + + return primaryKeyFingerprint == other.primaryKeyFingerprint && + subkeyFingerprint == other.subkeyFingerprint + } + + override fun hashCode(): Int { + return primaryKeyFingerprint.hashCode() + 31 * subkeyFingerprint.hashCode() + } + + override fun toString(): String = "$subkeyFingerprint $primaryKeyFingerprint" +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/_64DigitFingerprint.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/_64DigitFingerprint.kt new file mode 100644 index 00000000..a34dd880 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/_64DigitFingerprint.kt @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key + +import java.nio.Buffer +import java.nio.ByteBuffer +import java.nio.charset.Charset +import org.bouncycastle.openpgp.PGPKeyRing +import org.bouncycastle.openpgp.PGPPublicKey +import org.bouncycastle.openpgp.PGPSecretKey +import org.bouncycastle.util.encoders.Hex + +/** + * This class represents a hex encoded, upper case OpenPGP v5 or v6 fingerprint. Since both + * fingerprints use the same format, this class is used when parsing the fingerprint without knowing + * the key version. + */ +open class _64DigitFingerprint : OpenPgpFingerprint { + + /** + * Create a [_64DigitFingerprint]. + * + * @param fingerprint uppercase hexadecimal fingerprint of length 64 + */ + constructor(fingerprint: String) : super(fingerprint) + + constructor(bytes: ByteArray) : super(bytes) + + constructor(key: PGPPublicKey) : super(key) + + constructor(key: PGPSecretKey) : super(key) + + constructor(keys: PGPKeyRing) : super(keys) + + override val keyId: Long + get() { + val bytes = Hex.decode(fingerprint.toByteArray(Charset.forName("UTF-8"))) + val buf = ByteBuffer.wrap(bytes) + + // The key id is the left-most 8 bytes (conveniently a long). + // We have to cast here in order to be compatible with java 8 + // https://github.com/eclipse/jetty.project/issues/3244 + (buf as Buffer).position(0) + + return buf.getLong() + } + + override fun getVersion(): Int { + return -1 // might be v5 or v6 + } + + override fun isValid(fingerprint: String): Boolean { + return fingerprint.matches(("^[0-9A-F]{64}$".toRegex())) + } + + override fun toString(): String { + return super.toString() + } + + override fun prettyPrint(): String { + return buildString { + for (i in 0 until 4) { + append(fingerprint, i * 8, (i + 1) * 8).append(' ') + } + append(' ') + for (i in 4 until 7) { + append(fingerprint, i * 8, (i + 1) * 8).append(' ') + } + append(fingerprint, 56, 64) + } + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/certification/CertifyCertificate.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/certification/CertifyCertificate.kt new file mode 100644 index 00000000..9499355c --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/certification/CertifyCertificate.kt @@ -0,0 +1,245 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.certification + +import java.util.* +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPPublicKeyRing +import org.bouncycastle.openpgp.PGPSecretKey +import org.bouncycastle.openpgp.PGPSecretKeyRing +import org.bouncycastle.openpgp.PGPSignature +import org.pgpainless.PGPainless +import org.pgpainless.algorithm.CertificationType +import org.pgpainless.algorithm.KeyFlag +import org.pgpainless.algorithm.Trustworthiness +import org.pgpainless.exception.KeyException +import org.pgpainless.exception.KeyException.ExpiredKeyException +import org.pgpainless.exception.KeyException.MissingSecretKeyException +import org.pgpainless.exception.KeyException.RevokedKeyException +import org.pgpainless.key.protection.SecretKeyRingProtector +import org.pgpainless.key.util.KeyRingUtils +import org.pgpainless.signature.builder.ThirdPartyCertificationSignatureBuilder +import org.pgpainless.signature.builder.ThirdPartyDirectKeySignatureBuilder +import org.pgpainless.signature.subpackets.CertificationSubpackets + +/** + * API for creating certifications and delegations (Signatures) on keys. This API can be used to + * sign another persons OpenPGP key. + * + * A certification over a user-id is thereby used to attest, that the user believes that the user-id + * really belongs to the owner of the certificate. A delegation over a key can be used to delegate + * trust by marking the certificate as a trusted introducer. + */ +class CertifyCertificate { + + /** + * Create a certification over a User-Id. By default, this method will use + * [CertificationType.GENERIC] to create the signature. + * + * @param userId user-id to certify + * @param certificate certificate + * @return API + */ + fun userIdOnCertificate(userId: String, certificate: PGPPublicKeyRing): CertificationOnUserId = + userIdOnCertificate(userId, certificate, CertificationType.GENERIC) + + /** + * Create a certification of the given [CertificationType] over a User-Id. + * + * @param userid user-id to certify + * @param certificate certificate + * @param certificationType type of signature + * @return API + */ + fun userIdOnCertificate( + userId: String, + certificate: PGPPublicKeyRing, + certificationType: CertificationType + ) = CertificationOnUserId(userId, certificate, certificationType) + + /** + * Create a delegation (direct key signature) over a certificate. This can be used to mark a + * certificate as a trusted introducer (see [certificate] method with [Trustworthiness] + * argument). + * + * @param certificate certificate + * @return API + */ + fun certificate(certificate: PGPPublicKeyRing): DelegationOnCertificate = + certificate(certificate, null) + + /** + * Create a delegation (direct key signature) containing a + * [org.bouncycastle.bcpg.sig.TrustSignature] packet over a certificate. This can be used to + * mark a certificate as a trusted introducer. + * + * @param certificate certificate + * @param trustworthiness trustworthiness of the certificate + * @return API + */ + fun certificate(certificate: PGPPublicKeyRing, trustworthiness: Trustworthiness?) = + DelegationOnCertificate(certificate, trustworthiness) + + class CertificationOnUserId( + val userId: String, + val certificate: PGPPublicKeyRing, + val certificationType: CertificationType + ) { + + /** + * Create the certification using the given key. + * + * @param certificationKey key used to create the certification + * @param protector protector to unlock the certification key + * @return API + * @throws PGPException in case of an OpenPGP related error + */ + fun withKey( + certificationKey: PGPSecretKeyRing, + protector: SecretKeyRingProtector + ): CertificationOnUserIdWithSubpackets { + + val secretKey = getCertifyingSecretKey(certificationKey) + val sigBuilder = + ThirdPartyCertificationSignatureBuilder( + certificationType.asSignatureType(), secretKey, protector) + + return CertificationOnUserIdWithSubpackets(certificate, userId, sigBuilder) + } + } + + class CertificationOnUserIdWithSubpackets( + val certificate: PGPPublicKeyRing, + val userId: String, + val sigBuilder: ThirdPartyCertificationSignatureBuilder + ) { + + /** + * Apply the given signature subpackets and build the certification. + * + * @param subpacketCallback callback to modify the signatures subpackets + * @return result + * @throws PGPException in case of an OpenPGP related error + */ + fun buildWithSubpackets( + subpacketCallback: CertificationSubpackets.Callback + ): CertificationResult { + sigBuilder.applyCallback(subpacketCallback) + return build() + } + + /** + * Build the certification signature. + * + * @return result + * @throws PGPException in case of an OpenPGP related error + */ + fun build(): CertificationResult { + val signature = sigBuilder.build(certificate, userId) + val certifiedCertificate = + KeyRingUtils.injectCertification(certificate, userId, signature) + return CertificationResult(certifiedCertificate, signature) + } + } + + class DelegationOnCertificate( + val certificate: PGPPublicKeyRing, + val trustworthiness: Trustworthiness? + ) { + + /** + * Build the delegation using the given certification key. + * + * @param certificationKey key to create the certification with + * @param protector protector to unlock the certification key + * @return API + * @throws PGPException in case of an OpenPGP related error + */ + fun withKey( + certificationKey: PGPSecretKeyRing, + protector: SecretKeyRingProtector + ): DelegationOnCertificateWithSubpackets { + val secretKey = getCertifyingSecretKey(certificationKey) + val sigBuilder = ThirdPartyDirectKeySignatureBuilder(secretKey, protector) + if (trustworthiness != null) { + sigBuilder.hashedSubpackets.setTrust( + true, trustworthiness.depth, trustworthiness.amount) + } + return DelegationOnCertificateWithSubpackets(certificate, sigBuilder) + } + } + + class DelegationOnCertificateWithSubpackets( + val certificate: PGPPublicKeyRing, + val sigBuilder: ThirdPartyDirectKeySignatureBuilder + ) { + + /** + * Apply the given signature subpackets and build the delegation signature. + * + * @param subpacketsCallback callback to modify the signatures subpackets + * @return result + * @throws PGPException in case of an OpenPGP related error + */ + fun buildWithSubpackets( + subpacketsCallback: CertificationSubpackets.Callback + ): CertificationResult { + sigBuilder.applyCallback(subpacketsCallback) + return build() + } + + /** + * Build the delegation signature. + * + * @return result + * @throws PGPException in case of an OpenPGP related error + */ + fun build(): CertificationResult { + val delegatedKey = certificate.publicKey + val delegation = sigBuilder.build(delegatedKey) + val delegatedCertificate = + KeyRingUtils.injectCertification(certificate, delegatedKey, delegation) + return CertificationResult(delegatedCertificate, delegation) + } + } + + /** + * Result of a certification operation. + * + * @param certifiedCertificate certificate which now contains the newly created signature + * @param certification the newly created signature + */ + data class CertificationResult( + val certifiedCertificate: PGPPublicKeyRing, + val certification: PGPSignature + ) + + companion object { + @JvmStatic + private fun getCertifyingSecretKey(certificationKey: PGPSecretKeyRing): PGPSecretKey { + val now = Date() + val info = PGPainless.inspectKeyRing(certificationKey, now) + + val fingerprint = info.fingerprint + val certificationPubKey = info.getPublicKey(fingerprint) + requireNotNull(certificationPubKey) { "Primary key cannot be null." } + if (!info.isKeyValidlyBound(certificationPubKey.keyID)) { + throw RevokedKeyException(fingerprint) + } + + if (!info.isUsableForThirdPartyCertification) { + throw KeyException.UnacceptableThirdPartyCertificationKeyException(fingerprint) + } + + val expirationDate = info.getExpirationDateForUse(KeyFlag.CERTIFY_OTHER) + if (expirationDate != null && expirationDate < now) { + throw ExpiredKeyException(fingerprint, expirationDate) + } + + return certificationKey.getSecretKey(certificationPubKey.keyID) + ?: throw MissingSecretKeyException(fingerprint, certificationPubKey.keyID) + } + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/collection/PGPKeyRingCollection.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/collection/PGPKeyRingCollection.kt new file mode 100644 index 00000000..f69d4a08 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/collection/PGPKeyRingCollection.kt @@ -0,0 +1,116 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub , 2021 Flowcrypt a.s. +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.collection + +import java.io.InputStream +import org.bouncycastle.openpgp.* +import org.pgpainless.implementation.ImplementationFactory +import org.pgpainless.util.ArmorUtils + +/** + * This class describes a logic of handling a collection of different [PGPKeyRing]. The logic was + * inspired by [PGPSecretKeyRingCollection] and [PGPPublicKeyRingCollection]. + */ +class PGPKeyRingCollection( + val pgpSecretKeyRingCollection: PGPSecretKeyRingCollection, + val pgpPublicKeyRingCollection: PGPPublicKeyRingCollection +) { + + constructor(encoding: ByteArray, isSilent: Boolean) : this(encoding.inputStream(), isSilent) + + constructor(inputStream: InputStream, isSilent: Boolean) : this(parse(inputStream, isSilent)) + + constructor( + collection: Collection, + isSilent: Boolean + ) : this(segment(collection, isSilent)) + + private constructor( + arguments: Pair + ) : this(arguments.first, arguments.second) + + /** + * The number of rings in this collection. + * + * @return total size of [PGPSecretKeyRingCollection] plus [PGPPublicKeyRingCollection] in this + * collection + */ + val size: Int + get() = pgpSecretKeyRingCollection.size() + pgpPublicKeyRingCollection.size() + + fun size() = size + + @Deprecated("Wrong case of PGP -> Pgp", ReplaceWith("getPgpSecretKeyRingCollection()")) + fun getPGPSecretKeyRingCollection() = pgpSecretKeyRingCollection + + companion object { + @JvmStatic + private fun parse( + inputStream: InputStream, + isSilent: Boolean + ): Pair { + val secretKeyRings = mutableListOf() + val certificates = mutableListOf() + // Double getDecoderStream because of #96 + val objectFactory = + ImplementationFactory.getInstance() + .getPGPObjectFactory(ArmorUtils.getDecoderStream(inputStream)) + + for (obj in objectFactory) { + if (obj == null) { + break + } + + if (obj is PGPMarker) { + // Skip marker packets + continue + } + + if (obj is PGPSecretKeyRing) { + secretKeyRings.add(obj) + continue + } + + if (obj is PGPPublicKeyRing) { + certificates.add(obj) + continue + } + + if (!isSilent) { + throw PGPException( + "${obj.javaClass.name} found where ${PGPSecretKeyRing::class.java.simpleName}" + + " or ${PGPPublicKeyRing::class.java.simpleName} expected") + } + } + + return PGPSecretKeyRingCollection(secretKeyRings) to + PGPPublicKeyRingCollection(certificates) + } + + @JvmStatic + private fun segment( + collection: Collection, + isSilent: Boolean + ): Pair { + val secretKeyRings = mutableListOf() + val certificates = mutableListOf() + + for (keyRing in collection) { + if (keyRing is PGPSecretKeyRing) { + secretKeyRings.add(keyRing) + } else if (keyRing is PGPPublicKeyRing) { + certificates.add(keyRing) + } else if (!isSilent) { + throw PGPException( + "${keyRing.javaClass.name} found where ${PGPSecretKeyRing::class.java.simpleName}" + + " or ${PGPPublicKeyRing::class.java.simpleName} expected") + } + } + + return PGPSecretKeyRingCollection(secretKeyRings) to + PGPPublicKeyRingCollection(certificates) + } + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingBuilder.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingBuilder.kt new file mode 100644 index 00000000..aacfcceb --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingBuilder.kt @@ -0,0 +1,268 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.generation + +import java.io.IOException +import java.security.KeyPairGenerator +import java.util.* +import org.bouncycastle.openpgp.* +import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor +import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor +import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder +import org.bouncycastle.openpgp.operator.PGPDigestCalculator +import org.bouncycastle.util.Strings +import org.pgpainless.PGPainless +import org.pgpainless.algorithm.KeyFlag +import org.pgpainless.algorithm.SignatureType +import org.pgpainless.bouncycastle.extensions.unlock +import org.pgpainless.implementation.ImplementationFactory +import org.pgpainless.policy.Policy +import org.pgpainless.provider.ProviderFactory +import org.pgpainless.signature.subpackets.SelfSignatureSubpackets +import org.pgpainless.signature.subpackets.SignatureSubpackets +import org.pgpainless.signature.subpackets.SignatureSubpacketsHelper +import org.pgpainless.util.Passphrase + +class KeyRingBuilder : KeyRingBuilderInterface { + + private var primaryKeySpec: KeySpec? = null + private val subKeySpecs = mutableListOf() + private val userIds = mutableMapOf() + private var passphrase = Passphrase.emptyPassphrase() + private var expirationDate: Date? = Date(System.currentTimeMillis() + (5 * MILLIS_IN_YEAR)) + + override fun setPrimaryKey(keySpec: KeySpec): KeyRingBuilder = apply { + verifyKeySpecCompliesToPolicy(keySpec, PGPainless.getPolicy()) + verifyPrimaryKeyCanCertify(keySpec) + this.primaryKeySpec = keySpec + } + + override fun addSubkey(keySpec: KeySpec): KeyRingBuilder = apply { + verifyKeySpecCompliesToPolicy(keySpec, PGPainless.getPolicy()) + subKeySpecs.add(keySpec) + } + + override fun addUserId(userId: CharSequence): KeyRingBuilder = apply { + userIds[userId.toString()] = null + } + + override fun addUserId(userId: ByteArray): KeyRingBuilder = + addUserId(Strings.fromUTF8ByteArray(userId)) + + override fun setExpirationDate(expirationDate: Date?): KeyRingBuilder = apply { + if (expirationDate == null) { + this.expirationDate = null + return@apply + } + this.expirationDate = + expirationDate.let { + require(Date() < expirationDate) { "Expiration date must be in the future." } + expirationDate + } + } + + override fun setPassphrase(passphrase: Passphrase): KeyRingBuilder = apply { + this.passphrase = passphrase + } + + private fun verifyKeySpecCompliesToPolicy(keySpec: KeySpec, policy: Policy) { + val algorithm = keySpec.keyType.algorithm + val bitStrength = keySpec.keyType.bitStrength + require(policy.publicKeyAlgorithmPolicy.isAcceptable(algorithm, bitStrength)) { + "Public key algorithm policy violation: $algorithm with bit strength $bitStrength is not acceptable." + } + } + + private fun verifyPrimaryKeyCanCertify(spec: KeySpec) { + require(keyIsCertificationCapable(spec)) { + "Key algorithm ${spec.keyType.name} is not capable of creation certifications." + } + } + + private fun keyIsCertificationCapable(keySpec: KeySpec) = keySpec.keyType.canCertify + + override fun build(): PGPSecretKeyRing { + val keyFingerprintCalculator = ImplementationFactory.getInstance().v4FingerprintCalculator + val secretKeyEncryptor = buildSecretKeyEncryptor(keyFingerprintCalculator) + val secretKeyDecryptor = buildSecretKeyDecryptor() + + passphrase.clear() // Passphrase was used above, so we can get rid of it + + // generate primary key + requireNotNull(primaryKeySpec) { "Primary Key spec required." } + val certKey = generateKeyPair(primaryKeySpec!!) + val signer = buildContentSigner(certKey) + val signatureGenerator = PGPSignatureGenerator(signer) + + val hashedSubPacketGenerator = primaryKeySpec!!.subpacketGenerator + hashedSubPacketGenerator.setIssuerFingerprintAndKeyId(certKey.publicKey) + expirationDate?.let { hashedSubPacketGenerator.setKeyExpirationTime(certKey.publicKey, it) } + if (userIds.isNotEmpty()) { + hashedSubPacketGenerator.setPrimaryUserId() + } + + val generator = PGPSignatureSubpacketGenerator() + SignatureSubpacketsHelper.applyTo(hashedSubPacketGenerator, generator) + val hashedSubPackets = generator.generate() + val ringGenerator = + if (userIds.isEmpty()) { + PGPKeyRingGenerator( + certKey, + keyFingerprintCalculator, + hashedSubPackets, + null, + signer, + secretKeyEncryptor) + } else { + userIds.keys.first().let { primaryUserId -> + PGPKeyRingGenerator( + SignatureType.POSITIVE_CERTIFICATION.code, + certKey, + primaryUserId, + keyFingerprintCalculator, + hashedSubPackets, + null, + signer, + secretKeyEncryptor) + } + } + + addSubKeys(certKey, ringGenerator) + + // Generate secret key ring with only primary userId + val secretKeyRing = ringGenerator.generateSecretKeyRing() + val secretKeys = secretKeyRing.secretKeys + + // Attempt to add additional user-ids to the primary public key + var primaryPubKey = secretKeys.next().publicKey + val privateKey = secretKeyRing.secretKey.unlock(secretKeyDecryptor) + val userIdIterator = userIds.entries.iterator() + if (userIdIterator.hasNext()) { + userIdIterator.next() // Skip primary userId + } + while (userIdIterator.hasNext()) { + val additionalUserId = userIdIterator.next() + val userIdString = additionalUserId.key + val callback = additionalUserId.value + val subpackets = + if (callback == null) { + hashedSubPacketGenerator.also { it.setPrimaryUserId(null) } + } else { + SignatureSubpackets.createHashedSubpackets(primaryPubKey).also { + callback.modifyHashedSubpackets(it) + } + } + signatureGenerator.init(SignatureType.POSITIVE_CERTIFICATION.code, privateKey) + signatureGenerator.setHashedSubpackets(SignatureSubpacketsHelper.toVector(subpackets)) + val additionalUserIdSignature = + signatureGenerator.generateCertification(userIdString, primaryPubKey) + primaryPubKey = + PGPPublicKey.addCertification( + primaryPubKey, userIdString, additionalUserIdSignature) + } + + // Reassemble secret key ring with modified primary key + val primarySecretKey = + PGPSecretKey( + privateKey, primaryPubKey, keyFingerprintCalculator, true, secretKeyEncryptor) + val secretKeyList = mutableListOf(primarySecretKey) + while (secretKeys.hasNext()) { + secretKeyList.add(secretKeys.next()) + } + return PGPSecretKeyRing(secretKeyList) + } + + private fun addSubKeys(primaryKey: PGPKeyPair, ringGenerator: PGPKeyRingGenerator) { + for (subKeySpec in subKeySpecs) { + val subKey = generateKeyPair(subKeySpec) + if (subKeySpec.isInheritedSubPackets) { + ringGenerator.addSubKey(subKey) + } else { + var hashedSubpackets = subKeySpec.subpackets + try { + hashedSubpackets = + addPrimaryKeyBindingSignatureIfNecessary( + primaryKey, subKey, hashedSubpackets) + } catch (e: IOException) { + throw PGPException( + "Exception while adding primary key binding signature to signing subkey.", + e) + } + ringGenerator.addSubKey(subKey, hashedSubpackets, null) + } + } + } + + private fun addPrimaryKeyBindingSignatureIfNecessary( + primaryKey: PGPKeyPair, + subKey: PGPKeyPair, + hashedSubpackets: PGPSignatureSubpacketVector + ): PGPSignatureSubpacketVector { + val keyFlagMask = hashedSubpackets.keyFlags + if (!KeyFlag.hasKeyFlag(keyFlagMask, KeyFlag.SIGN_DATA) && + !KeyFlag.hasKeyFlag(keyFlagMask, KeyFlag.CERTIFY_OTHER)) { + return hashedSubpackets + } + + val bindingSignatureGenerator = PGPSignatureGenerator(buildContentSigner(subKey)) + bindingSignatureGenerator.init(SignatureType.PRIMARYKEY_BINDING.code, subKey.privateKey) + val primaryKeyBindingSig = + bindingSignatureGenerator.generateCertification(primaryKey.publicKey, subKey.publicKey) + val subpacketGenerator = PGPSignatureSubpacketGenerator(hashedSubpackets) + subpacketGenerator.addEmbeddedSignature(false, primaryKeyBindingSig) + return subpacketGenerator.generate() + } + + private fun buildContentSigner(certKey: PGPKeyPair): PGPContentSignerBuilder { + val hashAlgorithm = + PGPainless.getPolicy().certificationSignatureHashAlgorithmPolicy.defaultHashAlgorithm + return ImplementationFactory.getInstance() + .getPGPContentSignerBuilder(certKey.publicKey.algorithm, hashAlgorithm.algorithmId) + } + + private fun buildSecretKeyEncryptor( + keyFingerprintCalculator: PGPDigestCalculator + ): PBESecretKeyEncryptor? { + val keyEncryptionAlgorithm = + PGPainless.getPolicy() + .symmetricKeyEncryptionAlgorithmPolicy + .defaultSymmetricKeyAlgorithm + check(passphrase.isValid) { "Passphrase was cleared." } + return if (passphrase.isEmpty) null + else + ImplementationFactory.getInstance() + .getPBESecretKeyEncryptor( + keyEncryptionAlgorithm, keyFingerprintCalculator, passphrase) + } + + private fun buildSecretKeyDecryptor(): PBESecretKeyDecryptor? { + check(passphrase.isValid) { "Passphrase was cleared." } + return if (passphrase.isEmpty) null + else ImplementationFactory.getInstance().getPBESecretKeyDecryptor(passphrase) + } + + companion object { + const val MILLIS_IN_YEAR = 1000L * 60 * 60 * 24 * 365 + + @JvmStatic + @JvmOverloads + fun generateKeyPair( + spec: KeySpec, + creationTime: Date = spec.keyCreationDate ?: Date() + ): PGPKeyPair { + spec.keyType.let { type -> + // Create raw Key Pair + val keyPair = + KeyPairGenerator.getInstance(type.name, ProviderFactory.provider) + .also { it.initialize(type.algorithmSpec) } + .generateKeyPair() + + // Form PGP Key Pair + return ImplementationFactory.getInstance() + .getPGPKeyPair(type.algorithm, keyPair, creationTime) + } + } + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingBuilderInterface.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingBuilderInterface.kt new file mode 100644 index 00000000..ecc818b6 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingBuilderInterface.kt @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.generation + +import java.security.InvalidAlgorithmParameterException +import java.security.NoSuchAlgorithmException +import java.util.* +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPSecretKeyRing +import org.pgpainless.util.Passphrase + +interface KeyRingBuilderInterface> { + + fun setPrimaryKey(keySpec: KeySpec): B + + fun setPrimaryKey(builder: KeySpecBuilder): B = setPrimaryKey(builder.build()) + + fun addSubkey(keySpec: KeySpec): B + + fun addSubkey(builder: KeySpecBuilder): B = addSubkey(builder.build()) + + fun addUserId(userId: CharSequence): B + + fun addUserId(userId: ByteArray): B + + fun setExpirationDate(expirationDate: Date?): B + + fun setPassphrase(passphrase: Passphrase): B + + @Throws( + NoSuchAlgorithmException::class, + PGPException::class, + InvalidAlgorithmParameterException::class) + fun build(): PGPSecretKeyRing +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingTemplates.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingTemplates.kt new file mode 100644 index 00000000..82743661 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingTemplates.kt @@ -0,0 +1,213 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.generation + +import org.bouncycastle.openpgp.PGPSecretKeyRing +import org.pgpainless.PGPainless.Companion.buildKeyRing +import org.pgpainless.algorithm.KeyFlag +import org.pgpainless.key.generation.KeySpec.Companion.getBuilder +import org.pgpainless.key.generation.type.KeyType +import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve +import org.pgpainless.key.generation.type.rsa.RsaLength +import org.pgpainless.key.generation.type.xdh_legacy.XDHLegacySpec +import org.pgpainless.util.Passphrase + +class KeyRingTemplates { + + /** + * Generate an RSA OpenPGP key consisting of an RSA primary key used for certification, a + * dedicated RSA subkey used for signing and a third RSA subkey used for encryption. + * + * @param userId userId or null + * @param length length of the RSA keys + * @param passphrase passphrase to encrypt the key with. Can be empty for an unencrytped key. + * @return key + */ + @JvmOverloads + fun rsaKeyRing( + userId: CharSequence?, + length: RsaLength, + passphrase: Passphrase = Passphrase.emptyPassphrase() + ): PGPSecretKeyRing = + buildKeyRing() + .apply { + setPrimaryKey(getBuilder(KeyType.RSA(length), KeyFlag.CERTIFY_OTHER)) + addSubkey(getBuilder(KeyType.RSA(length), KeyFlag.SIGN_DATA)) + addSubkey( + getBuilder(KeyType.RSA(length), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)) + setPassphrase(passphrase) + if (userId != null) { + addUserId(userId) + } + } + .build() + + /** + * Generate an RSA OpenPGP key consisting of an RSA primary key used for certification, a + * dedicated RSA subkey used for signing and a third RSA subkey used for encryption. + * + * @param userId userId or null + * @param length length of the RSA keys + * @param password passphrase to encrypt the key with. Can be null or blank for unencrypted + * keys. + * @return key + */ + fun rsaKeyRing(userId: CharSequence?, length: RsaLength, password: String?): PGPSecretKeyRing = + password.let { + if (it.isNullOrBlank()) { + rsaKeyRing(userId, length, Passphrase.emptyPassphrase()) + } else { + rsaKeyRing(userId, length, Passphrase.fromPassword(it)) + } + } + + /** + * Creates a simple RSA KeyPair of length `length` with user-id `userId`. The KeyPair consists + * of a single RSA master key which is used for signing, encryption and certification. + * + * @param userId user id. + * @param length length in bits. + * @param password Password of the key. Can be empty for unencrypted keys. + * @return [PGPSecretKeyRing] containing the KeyPair. + */ + @JvmOverloads + fun simpleRsaKeyRing( + userId: CharSequence?, + length: RsaLength, + passphrase: Passphrase = Passphrase.emptyPassphrase() + ): PGPSecretKeyRing = + buildKeyRing() + .apply { + setPrimaryKey( + getBuilder( + KeyType.RSA(length), + KeyFlag.CERTIFY_OTHER, + KeyFlag.SIGN_DATA, + KeyFlag.ENCRYPT_COMMS)) + setPassphrase(passphrase) + if (userId != null) { + addUserId(userId.toString()) + } + } + .build() + + /** + * Creates a simple RSA KeyPair of length `length` with user-id `userId`. The KeyPair consists + * of a single RSA master key which is used for signing, encryption and certification. + * + * @param userId user id. + * @param length length in bits. + * @param password Password of the key. Can be null or blank for unencrypted keys. + * @return [PGPSecretKeyRing] containing the KeyPair. + */ + fun simpleRsaKeyRing(userId: CharSequence?, length: RsaLength, password: String?) = + password.let { + if (it.isNullOrBlank()) { + simpleRsaKeyRing(userId, length, Passphrase.emptyPassphrase()) + } else { + simpleRsaKeyRing(userId, length, Passphrase.fromPassword(it)) + } + } + + /** + * Creates a key ring consisting of an ed25519 EdDSA primary key and a X25519 XDH subkey. The + * EdDSA primary key is used for signing messages and certifying the sub key. The XDH subkey is + * used for encryption and decryption of messages. + * + * @param userId user-id + * @param passphrase Password of the private key. Can be empty for an unencrypted key. + * @return [PGPSecretKeyRing] containing the key pairs. + */ + @JvmOverloads + fun simpleEcKeyRing( + userId: CharSequence?, + passphrase: Passphrase = Passphrase.emptyPassphrase() + ): PGPSecretKeyRing = + buildKeyRing() + .apply { + setPrimaryKey( + getBuilder( + KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), + KeyFlag.CERTIFY_OTHER, + KeyFlag.SIGN_DATA)) + addSubkey( + getBuilder( + KeyType.XDH_LEGACY(XDHLegacySpec._X25519), + KeyFlag.ENCRYPT_STORAGE, + KeyFlag.ENCRYPT_COMMS)) + setPassphrase(passphrase) + if (userId != null) { + addUserId(userId.toString()) + } + } + .build() + + /** + * Creates a key ring consisting of an ed25519 EdDSA primary key and a X25519 XDH subkey. The + * EdDSA primary key is used for signing messages and certifying the sub key. The XDH subkey is + * used for encryption and decryption of messages. + * + * @param userId user-id + * @param passphrase Password of the private key. Can be null or blank for an unencrypted key. + * @return [PGPSecretKeyRing] containing the key pairs. + */ + fun simpleEcKeyRing(userId: CharSequence?, password: String?): PGPSecretKeyRing = + password.let { + if (it.isNullOrBlank()) { + simpleEcKeyRing(userId, Passphrase.emptyPassphrase()) + } else { + simpleEcKeyRing(userId, Passphrase.fromPassword(it)) + } + } + + /** + * Generate a modern PGP key ring consisting of an ed25519 EdDSA primary key which is used to + * certify an X25519 XDH encryption subkey and an ed25519 EdDSA signing key. + * + * @param userId primary user id + * @param passphrase passphrase for the private key. Can be empty for an unencrypted key. + * @return key ring + */ + @JvmOverloads + fun modernKeyRing( + userId: CharSequence?, + passphrase: Passphrase = Passphrase.emptyPassphrase() + ): PGPSecretKeyRing = + buildKeyRing() + .apply { + setPrimaryKey( + getBuilder( + KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER)) + addSubkey( + getBuilder( + KeyType.XDH_LEGACY(XDHLegacySpec._X25519), + KeyFlag.ENCRYPT_COMMS, + KeyFlag.ENCRYPT_STORAGE)) + addSubkey( + getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.SIGN_DATA)) + setPassphrase(passphrase) + if (userId != null) { + addUserId(userId) + } + } + .build() + + /** + * Generate a modern PGP key ring consisting of an ed25519 EdDSA primary key which is used to + * certify an X25519 XDH encryption subkey and an ed25519 EdDSA signing key. + * + * @param userId primary user id + * @param password passphrase for the private key. Can be null or blank for an unencrypted key. + * @return key ring + */ + fun modernKeyRing(userId: CharSequence?, password: String?): PGPSecretKeyRing = + password.let { + if (it.isNullOrBlank()) { + modernKeyRing(userId, Passphrase.emptyPassphrase()) + } else { + modernKeyRing(userId, Passphrase.fromPassword(it)) + } + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeySpec.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeySpec.kt new file mode 100644 index 00000000..f616a7f2 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeySpec.kt @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.generation + +import java.util.* +import org.bouncycastle.openpgp.PGPSignatureSubpacketVector +import org.pgpainless.algorithm.KeyFlag +import org.pgpainless.key.generation.type.KeyType +import org.pgpainless.signature.subpackets.SignatureSubpackets +import org.pgpainless.signature.subpackets.SignatureSubpacketsHelper + +data class KeySpec( + val keyType: KeyType, + val subpacketGenerator: SignatureSubpackets, + val isInheritedSubPackets: Boolean, + val keyCreationDate: Date? +) { + + val subpackets: PGPSignatureSubpacketVector + get() = SignatureSubpacketsHelper.toVector(subpacketGenerator) + + companion object { + @JvmStatic + fun getBuilder(type: KeyType, vararg flags: KeyFlag) = KeySpecBuilder(type, *flags) + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeySpecBuilder.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeySpecBuilder.kt new file mode 100644 index 00000000..0e7f9aae --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeySpecBuilder.kt @@ -0,0 +1,69 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.generation + +import java.util.* +import org.pgpainless.PGPainless +import org.pgpainless.algorithm.* +import org.pgpainless.key.generation.type.KeyType +import org.pgpainless.signature.subpackets.SelfSignatureSubpackets +import org.pgpainless.signature.subpackets.SignatureSubpackets +import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil + +class KeySpecBuilder +constructor( + private val type: KeyType, + private val keyFlags: List, +) : KeySpecBuilderInterface { + + private val hashedSubpackets: SelfSignatureSubpackets = SignatureSubpackets() + private val algorithmSuite: AlgorithmSuite = PGPainless.getPolicy().keyGenerationAlgorithmSuite + private var preferredCompressionAlgorithms: Set = + algorithmSuite.compressionAlgorithms + private var preferredHashAlgorithms: Set = algorithmSuite.hashAlgorithms + private var preferredSymmetricAlgorithms: Set = + algorithmSuite.symmetricKeyAlgorithms + private var keyCreationDate: Date? = null + + constructor(type: KeyType, vararg keyFlags: KeyFlag) : this(type, listOf(*keyFlags)) + + init { + SignatureSubpacketsUtil.assureKeyCanCarryFlags(type, *keyFlags.toTypedArray()) + } + + override fun overridePreferredCompressionAlgorithms( + vararg algorithms: CompressionAlgorithm + ): KeySpecBuilder = apply { this.preferredCompressionAlgorithms = algorithms.toSet() } + + override fun overridePreferredHashAlgorithms(vararg algorithms: HashAlgorithm): KeySpecBuilder = + apply { + this.preferredHashAlgorithms = algorithms.toSet() + } + + override fun overridePreferredSymmetricKeyAlgorithms( + vararg algorithms: SymmetricKeyAlgorithm + ): KeySpecBuilder = apply { + require(!algorithms.contains(SymmetricKeyAlgorithm.NULL)) { + "NULL (unencrypted) is an invalid symmetric key algorithm preference." + } + this.preferredSymmetricAlgorithms = algorithms.toSet() + } + + override fun setKeyCreationDate(creationDate: Date): KeySpecBuilder = apply { + this.keyCreationDate = creationDate + } + + override fun build(): KeySpec { + return hashedSubpackets + .apply { + setKeyFlags(keyFlags) + setPreferredCompressionAlgorithms(preferredCompressionAlgorithms) + setPreferredHashAlgorithms(preferredHashAlgorithms) + setPreferredSymmetricKeyAlgorithms(preferredSymmetricAlgorithms) + setFeatures(Feature.MODIFICATION_DETECTION) + } + .let { KeySpec(type, hashedSubpackets as SignatureSubpackets, false, keyCreationDate) } + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeySpecBuilderInterface.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeySpecBuilderInterface.kt new file mode 100644 index 00000000..7fb767e4 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeySpecBuilderInterface.kt @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.generation + +import java.util.* +import org.pgpainless.algorithm.CompressionAlgorithm +import org.pgpainless.algorithm.HashAlgorithm +import org.pgpainless.algorithm.SymmetricKeyAlgorithm + +interface KeySpecBuilderInterface { + + fun overridePreferredCompressionAlgorithms( + vararg algorithms: CompressionAlgorithm + ): KeySpecBuilder + + fun overridePreferredHashAlgorithms(vararg algorithms: HashAlgorithm): KeySpecBuilder + + fun overridePreferredSymmetricKeyAlgorithms( + vararg algorithms: SymmetricKeyAlgorithm + ): KeySpecBuilder + + fun setKeyCreationDate(creationDate: Date): KeySpecBuilder + + fun build(): KeySpec +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/KeyLength.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/KeyLength.kt new file mode 100644 index 00000000..1ff63604 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/KeyLength.kt @@ -0,0 +1,10 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.generation.type + +interface KeyLength { + + val length: Int +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/KeyType.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/KeyType.kt new file mode 100644 index 00000000..c7691f46 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/KeyType.kt @@ -0,0 +1,107 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.generation.type + +import java.security.spec.AlgorithmParameterSpec +import org.pgpainless.algorithm.PublicKeyAlgorithm +import org.pgpainless.key.generation.type.ecc.EllipticCurve +import org.pgpainless.key.generation.type.ecc.ecdh.ECDH +import org.pgpainless.key.generation.type.ecc.ecdsa.ECDSA +import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacy +import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve +import org.pgpainless.key.generation.type.rsa.RSA +import org.pgpainless.key.generation.type.rsa.RsaLength +import org.pgpainless.key.generation.type.xdh_legacy.XDHLegacy +import org.pgpainless.key.generation.type.xdh_legacy.XDHLegacySpec + +@Suppress("INAPPLICABLE_JVM_NAME") // https://youtrack.jetbrains.com/issue/KT-31420 +interface KeyType { + + /** + * Return the encryption algorithm name. + * + * @return algorithm name. + */ + val name: String + + /** + * Return the public key algorithm. + * + * @return public key algorithm + */ + val algorithm: PublicKeyAlgorithm + + /** + * Return the strength of the key in bits. + * + * @return strength of the key in bits + */ + val bitStrength: Int + + /** + * Return an implementation of [AlgorithmParameterSpec] that can be used to generate the key. + * + * @return algorithm parameter spec + */ + val algorithmSpec: AlgorithmParameterSpec + + /** + * Return true if the key that is generated from this type is able to carry the SIGN_DATA key + * flag. See [org.pgpainless.algorithm.KeyFlag.SIGN_DATA]. + * + * @return true if the key can sign. + */ + val canSign: Boolean + @JvmName("canSign") get() = algorithm.signingCapable + + /** + * Return true if the key that is generated from this type is able to carry the CERTIFY_OTHER + * key flag. See [org.pgpainless.algorithm.KeyFlag.CERTIFY_OTHER]. + * + * @return true if the key is able to certify other keys + */ + val canCertify: Boolean + @JvmName("canCertify") get() = canSign + + /** + * Return true if the key that is generated from this type is able to carry the AUTHENTICATION + * key flag. See [org.pgpainless.algorithm.KeyFlag.AUTHENTICATION]. + * + * @return true if the key can be used for authentication purposes. + */ + val canAuthenticate: Boolean + @JvmName("canAuthenticate") get() = canSign + + /** + * Return true if the key that is generated from this type is able to carry the ENCRYPT_COMMS + * key flag. See [org.pgpainless.algorithm.KeyFlag.ENCRYPT_COMMS]. + * + * @return true if the key can encrypt communication + */ + val canEncryptCommunication: Boolean + @JvmName("canEncryptCommunication") get() = algorithm.encryptionCapable + + /** + * Return true if the key that is generated from this type is able to carry the ENCRYPT_STORAGE + * key flag. See [org.pgpainless.algorithm.KeyFlag.ENCRYPT_STORAGE]. + * + * @return true if the key can encrypt for storage + */ + val canEncryptStorage: Boolean + @JvmName("canEncryptStorage") get() = algorithm.encryptionCapable + + companion object { + @JvmStatic fun RSA(length: RsaLength): RSA = RSA.withLength(length) + + @JvmStatic fun ECDH(curve: EllipticCurve): ECDH = ECDH.fromCurve(curve) + + @JvmStatic fun ECDSA(curve: EllipticCurve): ECDSA = ECDSA.fromCurve(curve) + + @JvmStatic + fun EDDSA_LEGACY(curve: EdDSALegacyCurve): EdDSALegacy = EdDSALegacy.fromCurve(curve) + + @JvmStatic fun XDH_LEGACY(curve: XDHLegacySpec): XDHLegacy = XDHLegacy.fromSpec(curve) + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/ecc/EllipticCurve.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/ecc/EllipticCurve.kt new file mode 100644 index 00000000..9e229462 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/ecc/EllipticCurve.kt @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.generation.type.ecc + +/** + * Elliptic curves for use with [org.pgpainless.key.generation.type.ecc.ecdh.ECDH] and + * [org.pgpainless.key.generation.type.ecc.ecdsa.ECDSA]. For curve25519 related curve definitions + * see [XDHSpec] and [org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve]. + */ +enum class EllipticCurve(val curveName: String, val bitStrength: Int) { + _P256("prime256v1", 256), // prime256v1 is equivalent to P-256, see + // https://tools.ietf.org/search/rfc4492#page-32 + _P384( + "secp384r1", + 384), // secp384r1 is equivalent to P-384, see https://tools.ietf.org/search/rfc4492#page-32 + _P521( + "secp521r1", + 521), // secp521r1 is equivalent to P-521, see https://tools.ietf.org/search/rfc4492#page-32 + _BRAINPOOLP256R1("brainpoolP256r1", 256), + _BRAINPOOLP384R1("brainpoolP384r1", 384), + _BRAINPOOLP512R1("brainpoolP512r1", 512), + ; + + fun getName(): String = curveName +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/ecc/ecdh/ECDH.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/ecc/ecdh/ECDH.kt new file mode 100644 index 00000000..04e196e0 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/ecc/ecdh/ECDH.kt @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.generation.type.ecc.ecdh + +import org.bouncycastle.jce.spec.ECNamedCurveGenParameterSpec +import org.pgpainless.algorithm.PublicKeyAlgorithm +import org.pgpainless.key.generation.type.KeyType +import org.pgpainless.key.generation.type.ecc.EllipticCurve + +class ECDH private constructor(val curve: EllipticCurve) : KeyType { + override val name = "ECDH" + override val algorithm = PublicKeyAlgorithm.ECDH + override val bitStrength = curve.bitStrength + override val algorithmSpec = ECNamedCurveGenParameterSpec(curve.curveName) + + companion object { + @JvmStatic fun fromCurve(curve: EllipticCurve) = ECDH(curve) + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/ecc/ecdsa/ECDSA.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/ecc/ecdsa/ECDSA.kt new file mode 100644 index 00000000..1784b49d --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/ecc/ecdsa/ECDSA.kt @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.generation.type.ecc.ecdsa + +import org.bouncycastle.jce.spec.ECNamedCurveGenParameterSpec +import org.pgpainless.algorithm.PublicKeyAlgorithm +import org.pgpainless.key.generation.type.KeyType +import org.pgpainless.key.generation.type.ecc.EllipticCurve + +class ECDSA private constructor(val curve: EllipticCurve) : KeyType { + override val name = "ECDSA" + override val algorithm = PublicKeyAlgorithm.ECDSA + override val bitStrength = curve.bitStrength + override val algorithmSpec = ECNamedCurveGenParameterSpec(curve.curveName) + + companion object { + @JvmStatic fun fromCurve(curve: EllipticCurve) = ECDSA(curve) + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/eddsa_legacy/EdDSALegacy.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/eddsa_legacy/EdDSALegacy.kt new file mode 100644 index 00000000..e177de68 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/eddsa_legacy/EdDSALegacy.kt @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.generation.type.eddsa_legacy + +import org.bouncycastle.jce.spec.ECNamedCurveGenParameterSpec +import org.pgpainless.algorithm.PublicKeyAlgorithm +import org.pgpainless.key.generation.type.KeyType + +class EdDSALegacy private constructor(val curve: EdDSALegacyCurve) : KeyType { + override val name = "EdDSA" + override val algorithm = PublicKeyAlgorithm.EDDSA_LEGACY + override val bitStrength = curve.bitStrength + override val algorithmSpec = ECNamedCurveGenParameterSpec(curve.curveName) + + companion object { + @JvmStatic fun fromCurve(curve: EdDSALegacyCurve) = EdDSALegacy(curve) + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/eddsa_legacy/EdDSALegacyCurve.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/eddsa_legacy/EdDSALegacyCurve.kt new file mode 100644 index 00000000..2a32b8aa --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/eddsa_legacy/EdDSALegacyCurve.kt @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.generation.type.eddsa_legacy + +enum class EdDSALegacyCurve(val curveName: String, val bitStrength: Int) { + _Ed25519("ed25519", 256), + ; + + fun getName() = curveName +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/elgamal/ElGamal.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/elgamal/ElGamal.kt new file mode 100644 index 00000000..d925fc3d --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/elgamal/ElGamal.kt @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.generation.type.elgamal + +import org.bouncycastle.jce.spec.ElGamalParameterSpec +import org.pgpainless.algorithm.PublicKeyAlgorithm +import org.pgpainless.key.generation.type.KeyType + +/** + * ElGamal encryption only key type. + * + * @deprecated the use of ElGamal is not recommended anymore. + */ +@Deprecated("The use of ElGamal is not recommended anymore.") +class ElGamal private constructor(length: ElGamalLength) : KeyType { + + override val name = "ElGamal" + override val algorithm = PublicKeyAlgorithm.ELGAMAL_ENCRYPT + override val bitStrength = length.length + override val algorithmSpec = ElGamalParameterSpec(length.p, length.g) + + companion object { + @JvmStatic fun withLength(length: ElGamalLength) = ElGamal(length) + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/elgamal/ElGamalLength.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/elgamal/ElGamalLength.kt new file mode 100644 index 00000000..2d29b88d --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/elgamal/ElGamalLength.kt @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.generation.type.elgamal + +import java.math.BigInteger +import org.pgpainless.key.generation.type.KeyLength + +/** + * The following primes are taken from RFC-3526. + * + * @see RFC-3526: More Modular Exponential (MODP) + * Diffie-Hellman groups for Internet Key Exchange (IKE) + * @deprecated the use of ElGamal keys is no longer recommended. + */ +@Deprecated("The use of ElGamal keys is no longer recommended.") +enum class ElGamalLength(override val length: Int, p: String, g: String) : KeyLength { + + /** prime: 2^1536 - 2^1472 - 1 + 2^64 * { [2^1406 pi] + 741804 }. generator: 2 */ + _1536( + 1536, + "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA237327FFFFFFFFFFFFFFFF", + "2"), + + /** prime: 2^2048 - 2^1984 - 1 + 2^64 * { [2^1918 pi] + 124476 }. generator: 2 */ + _2048( + 2048, + "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AACAA68FFFFFFFFFFFFFFFF", + "2"), + + /** prime: 2^3072 - 2^3008 - 1 + 2^64 * { [2^2942 pi] + 1690314 }. generator: 2 */ + _3072( + 3072, + "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B18177B200CBBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFCE0FD108E4B82D120A93AD2CAFFFFFFFFFFFFFFFF", + "2"), + + /** prime: 2^4096 - 2^4032 - 1 + 2^64 * { [2^3966 pi] + 240904 }. generator: 2 */ + _4096( + 4096, + "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B18177B200CBBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFCE0FD108E4B82D120A92108011A723C12A787E6D788719A10BDBA5B2699C327186AF4E23C1A946834B6150BDA2583E9CA2AD44CE8DBBBC2DB04DE8EF92E8EFC141FBECAA6287C59474E6BC05D99B2964FA090C3A2233BA186515BE7ED1F612970CEE2D7AFB81BDD762170481CD0069127D5B05AA993B4EA988D8FDDC186FFB7DC90A6C08F4DF435C934063199FFFFFFFFFFFFFFFF", + "2"), + + /** prime: 2^6144 - 2^6080 - 1 + 2^64 * { [2^6014 pi] + 929484 }. generator: 2 */ + _6144( + 6144, + "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B18177B200CBBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFCE0FD108E4B82D120A92108011A723C12A787E6D788719A10BDBA5B2699C327186AF4E23C1A946834B6150BDA2583E9CA2AD44CE8DBBBC2DB04DE8EF92E8EFC141FBECAA6287C59474E6BC05D99B2964FA090C3A2233BA186515BE7ED1F612970CEE2D7AFB81BDD762170481CD0069127D5B05AA993B4EA988D8FDDC186FFB7DC90A6C08F4DF435C93402849236C3FAB4D27C7026C1D4DCB2602646DEC9751E763DBA37BDF8FF9406AD9E530EE5DB382F413001AEB06A53ED9027D831179727B0865A8918DA3EDBEBCF9B14ED44CE6CBACED4BB1BDB7F1447E6CC254B332051512BD7AF426FB8F401378CD2BF5983CA01C64B92ECF032EA15D1721D03F482D7CE6E74FEF6D55E702F46980C82B5A84031900B1C9E59E7C97FBEC7E8F323A97A7E36CC88BE0F1D45B7FF585AC54BD407B22B4154AACC8F6D7EBF48E1D814CC5ED20F8037E0A79715EEF29BE32806A1D58BB7C5DA76F550AA3D8A1FBFF0EB19CCB1A313D55CDA56C9EC2EF29632387FE8D76E3C0468043E8F663F4860EE12BF2D5B0B7474D6E694F91E6DCC4024FFFFFFFFFFFFFFFF", + "2"), + + /** prime: 2^8192 - 2^8128 - 1 + 2^64 * { [2^8062 pi] + 4743158 }. generator: 2 */ + _8192( + 8192, + "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B18177B200CBBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFCE0FD108E4B82D120A92108011A723C12A787E6D788719A10BDBA5B2699C327186AF4E23C1A946834B6150BDA2583E9CA2AD44CE8DBBBC2DB04DE8EF92E8EFC141FBECAA6287C59474E6BC05D99B2964FA090C3A2233BA186515BE7ED1F612970CEE2D7AFB81BDD762170481CD0069127D5B05AA993B4EA988D8FDDC186FFB7DC90A6C08F4DF435C93402849236C3FAB4D27C7026C1D4DCB2602646DEC9751E763DBA37BDF8FF9406AD9E530EE5DB382F413001AEB06A53ED9027D831179727B0865A8918DA3EDBEBCF9B14ED44CE6CBACED4BB1BDB7F1447E6CC254B332051512BD7AF426FB8F401378CD2BF5983CA01C64B92ECF032EA15D1721D03F482D7CE6E74FEF6D55E702F46980C82B5A84031900B1C9E59E7C97FBEC7E8F323A97A7E36CC88BE0F1D45B7FF585AC54BD407B22B4154AACC8F6D7EBF48E1D814CC5ED20F8037E0A79715EEF29BE32806A1D58BB7C5DA76F550AA3D8A1FBFF0EB19CCB1A313D55CDA56C9EC2EF29632387FE8D76E3C0468043E8F663F4860EE12BF2D5B0B7474D6E694F91E6DBE115974A3926F12FEE5E438777CB6A932DF8CD8BEC4D073B931BA3BC832B68D9DD300741FA7BF8AFC47ED2576F6936BA424663AAB639C5AE4F5683423B4742BF1C978238F16CBE39D652DE3FDB8BEFC848AD922222E04A4037C0713EB57A81A23F0C73473FC646CEA306B4BCBC8862F8385DDFA9D4B7FA2C087E879683303ED5BDD3A062B3CF5B3A278A66D2A13F83F44F82DDF310EE074AB6A364597E899A0255DC164F31CC50846851DF9AB48195DED7EA1B1D510BD7EE74D73FAF36BC31ECFA268359046F4EB879F924009438B481C6CD7889A002ED5EE382BC9190DA6FC026E479558E4475677E9AA9E3050E2765694DFC81F56E880B96E7160C980DD98EDD3DFFFFFFFFFFFFFFFFF", + "2"); + + val p: BigInteger + val g: BigInteger + + init { + this.p = BigInteger(p, 16) + this.g = BigInteger(g, 16) + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/rsa/RSA.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/rsa/RSA.kt new file mode 100644 index 00000000..39ddbbbb --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/rsa/RSA.kt @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.generation.type.rsa + +import java.security.spec.RSAKeyGenParameterSpec +import org.pgpainless.algorithm.PublicKeyAlgorithm +import org.pgpainless.key.generation.type.KeyType + +/** Key type that specifies the RSA_GENERAL algorithm. */ +class RSA private constructor(length: RsaLength) : KeyType { + + override val name = "RSA" + override val algorithm = PublicKeyAlgorithm.RSA_GENERAL + override val bitStrength = length.length + override val algorithmSpec = RSAKeyGenParameterSpec(length.length, RSAKeyGenParameterSpec.F4) + + companion object { + @JvmStatic fun withLength(length: RsaLength) = RSA(length) + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/rsa/RsaLength.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/rsa/RsaLength.kt new file mode 100644 index 00000000..7837a1f5 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/rsa/RsaLength.kt @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.generation.type.rsa + +import org.pgpainless.key.generation.type.KeyLength + +enum class RsaLength(override val length: Int) : KeyLength { + @Deprecated("1024 bits are considered too weak for RSA nowadays.", ReplaceWith("_3072")) + _1024(1024), + @Deprecated("2048 bits are considered too weak for RSA nowadays.", ReplaceWith("_3072")) + _2048(2048), + _3072(3072), + _4096(4096), + _8192(8192) +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/xdh_legacy/XDHLegacy.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/xdh_legacy/XDHLegacy.kt new file mode 100644 index 00000000..4f0408bc --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/xdh_legacy/XDHLegacy.kt @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.generation.type.xdh_legacy + +import org.bouncycastle.jce.spec.ECNamedCurveGenParameterSpec +import org.pgpainless.algorithm.PublicKeyAlgorithm +import org.pgpainless.key.generation.type.KeyType + +class XDHLegacy private constructor(spec: XDHLegacySpec) : KeyType { + override val name = "XDH" + override val algorithm = PublicKeyAlgorithm.ECDH + override val bitStrength = spec.bitStrength + override val algorithmSpec = ECNamedCurveGenParameterSpec(spec.algorithmName) + + companion object { + @JvmStatic fun fromSpec(spec: XDHLegacySpec) = XDHLegacy(spec) + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/xdh_legacy/XDHLegacySpec.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/xdh_legacy/XDHLegacySpec.kt new file mode 100644 index 00000000..cb634850 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/xdh_legacy/XDHLegacySpec.kt @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.generation.type.xdh_legacy + +enum class XDHLegacySpec(val algorithmName: String, val curveName: String, val bitStrength: Int) { + _X25519("X25519", "curve25519", 256), + ; + + fun getName() = algorithmName +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyAccessor.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyAccessor.kt new file mode 100644 index 00000000..935c4f48 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyAccessor.kt @@ -0,0 +1,89 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.info + +import org.bouncycastle.openpgp.PGPSignature +import org.pgpainless.algorithm.CompressionAlgorithm +import org.pgpainless.algorithm.HashAlgorithm +import org.pgpainless.algorithm.SymmetricKeyAlgorithm +import org.pgpainless.key.SubkeyIdentifier +import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil + +abstract class KeyAccessor(protected val info: KeyRingInfo, protected val key: SubkeyIdentifier) { + + /** + * Depending on the way we address the key (key-id or user-id), return the respective + * [PGPSignature] which contains the algorithm preferences we are going to use. + * + *

+ * If we address a key via its user-id, we want to rely on the algorithm preferences in the + * user-id certification, while we would instead rely on those in the direct-key signature if + * we'd address the key by key-id. + * + * @return signature + */ + abstract val signatureWithPreferences: PGPSignature + + /** Preferred symmetric key encryption algorithms. */ + val preferredSymmetricKeyAlgorithms: Set + get() = + SignatureSubpacketsUtil.parsePreferredSymmetricKeyAlgorithms(signatureWithPreferences) + + /** Preferred hash algorithms. */ + val preferredHashAlgorithms: Set + get() = SignatureSubpacketsUtil.parsePreferredHashAlgorithms(signatureWithPreferences) + + /** Preferred compression algorithms. */ + val preferredCompressionAlgorithms: Set + get() = + SignatureSubpacketsUtil.parsePreferredCompressionAlgorithms(signatureWithPreferences) + + /** + * Address the key via a user-id (e.g. `Alice `). In this case we are + * sourcing preferred algorithms from the user-id certification first. + */ + class ViaUserId(info: KeyRingInfo, key: SubkeyIdentifier, private val userId: CharSequence) : + KeyAccessor(info, key) { + override val signatureWithPreferences: PGPSignature + get() = + checkNotNull(info.getLatestUserIdCertification(userId.toString())) { + "No valid user-id certification signature found for '$userId'." + } + } + + /** + * Address the key via key-id. In this case we are sourcing preferred algorithms from the keys + * direct-key signature first. + */ + class ViaKeyId(info: KeyRingInfo, key: SubkeyIdentifier) : KeyAccessor(info, key) { + override val signatureWithPreferences: PGPSignature + get() { + // If the key is located by Key ID, the algorithm of the primary User ID of the key + // provides the + // preferred symmetric algorithm. + info.primaryUserId?.let { userId -> + info.getLatestUserIdCertification(userId).let { if (it != null) return it } + } + + return info.getCurrentSubkeyBindingSignature(key.subkeyId) + ?: throw NoSuchElementException( + "Key does not carry acceptable self-signature signature.") + } + } + + class SubKey(info: KeyRingInfo, key: SubkeyIdentifier) : KeyAccessor(info, key) { + override val signatureWithPreferences: PGPSignature + get() = + checkNotNull( + if (key.isPrimaryKey) { + info.latestDirectKeySelfSignature + ?: info.primaryUserId?.let { info.getLatestUserIdCertification(it) } + } else { + info.getCurrentSubkeyBindingSignature(key.subkeyId) + }) { + "No valid signature found." + } + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyInfo.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyInfo.kt new file mode 100644 index 00000000..75a35140 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyInfo.kt @@ -0,0 +1,81 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub , 2021 Flowcrypt a.s. +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.info + +import org.bouncycastle.openpgp.PGPPublicKey +import org.bouncycastle.openpgp.PGPSecretKey +import org.pgpainless.bouncycastle.extensions.getCurveName +import org.pgpainless.bouncycastle.extensions.hasDummyS2K +import org.pgpainless.bouncycastle.extensions.isDecrypted +import org.pgpainless.bouncycastle.extensions.isEncrypted + +@Deprecated("Deprecated in favor of extension functions to PGPSecretKey and PGPPublicKey.") +class KeyInfo private constructor(val secretKey: PGPSecretKey?, val publicKey: PGPPublicKey) { + + constructor(secretKey: PGPSecretKey) : this(secretKey, secretKey.publicKey) + + constructor(publicKey: PGPPublicKey) : this(null, publicKey) + + /** + * Return the name of the elliptic curve used by this key, or throw an + * [IllegalArgumentException] if the key is not based on elliptic curves, or on an unknown + * curve. + */ + @Deprecated( + "Deprecated in favor of calling getCurveName() on the PGPPublicKey itself.", + ReplaceWith("publicKey.getCurveName()")) + val curveName: String + get() = publicKey.getCurveName() + + /** + * Return true, if the secret key is encrypted. This method returns false, if the secret key is + * null. + */ + @Deprecated( + "Deprecated in favor of calling isEncrypted() on the PGPSecretKey itself.", + ReplaceWith("secretKey.isEncrypted()")) + val isEncrypted: Boolean + get() = secretKey?.isEncrypted() ?: false + + /** + * Return true, if the secret key is decrypted. This method returns true, if the secret key is + * null. + */ + @Deprecated( + "Deprecated in favor of calling isDecrypted() on the PGPSecretKey itself.", + ReplaceWith("secretKey.isDecrypted()")) + val isDecrypted: Boolean + get() = secretKey?.isDecrypted() ?: true + + /** + * Return true, if the secret key is using the GNU_DUMMY_S2K s2k type. This method returns + * false, if the secret key is null. + */ + @Deprecated( + "Deprecated in favor of calling hasDummyS2K() on the PGPSecretKey itself.", + ReplaceWith("secretKey.hasDummyS2K()")) + val hasDummyS2K: Boolean + @JvmName("hasDummyS2K") get() = secretKey?.hasDummyS2K() ?: false + + companion object { + @JvmStatic + @Deprecated( + "Deprecated in favor of calling isEncrypted() on the PGPSecretKey itself.", + ReplaceWith("secretKey.isEncrypted()")) + fun isEncrypted(secretKey: PGPSecretKey?) = secretKey.isEncrypted() + + @JvmStatic + @Deprecated( + "Deprecated in favor of calling isDecrypted() on the PGPSecretKey itself.", + ReplaceWith("secretKey.isDecrypted()")) + fun isDecrypted(secretKey: PGPSecretKey?) = secretKey.isDecrypted() + + @JvmStatic + @Deprecated( + "Deprecated in favor of calling hasDummyS2K() on the PGPSecretKey itself.", + ReplaceWith("secretKey.hasDummyS2K()")) + fun hasDummyS2K(secretKey: PGPSecretKey?) = secretKey.hasDummyS2K() + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyRingInfo.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyRingInfo.kt new file mode 100644 index 00000000..ce4fbe56 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyRingInfo.kt @@ -0,0 +1,766 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.info + +import java.util.* +import openpgp.openPgpKeyId +import org.bouncycastle.openpgp.* +import org.pgpainless.PGPainless +import org.pgpainless.algorithm.* +import org.pgpainless.bouncycastle.extensions.* +import org.pgpainless.exception.KeyException.UnboundUserIdException +import org.pgpainless.key.OpenPgpFingerprint +import org.pgpainless.key.SubkeyIdentifier +import org.pgpainless.key.util.KeyRingUtils +import org.pgpainless.policy.Policy +import org.pgpainless.signature.consumer.SignaturePicker +import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil +import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil.Companion.getKeyExpirationTimeAsDate +import org.pgpainless.util.DateUtil +import org.slf4j.LoggerFactory + +class KeyRingInfo( + val keys: PGPKeyRing, + val policy: Policy = PGPainless.getPolicy(), + val referenceDate: Date = Date() +) { + + @JvmOverloads + constructor( + keys: PGPKeyRing, + referenceDate: Date = Date() + ) : this(keys, PGPainless.getPolicy(), referenceDate) + + private val signatures: Signatures = Signatures(keys, referenceDate, policy) + + /** Primary [PGPPublicKey]. */ + val publicKey: PGPPublicKey = KeyRingUtils.requirePrimaryPublicKeyFrom(keys) + + /** Primary key ID. */ + val keyId: Long = publicKey.keyID + + /** Primary key fingerprint. */ + val fingerprint: OpenPgpFingerprint = OpenPgpFingerprint.of(keys) + + /** All User-IDs (valid, expired, revoked). */ + val userIds: List = KeyRingUtils.getUserIdsIgnoringInvalidUTF8(publicKey) + + /** Primary User-ID. */ + val primaryUserId = findPrimaryUserId() + + /** Revocation State. */ + val revocationState = signatures.primaryKeyRevocation.toRevocationState() + /** + * Return the date on which the primary key was revoked, or null if it has not yet been revoked. + * + * @return revocation date or null + */ + val revocationDate: Date? = + if (revocationState.isSoftRevocation()) revocationState.date else null + + /** + * Primary [PGPSecretKey] of this key ring or null if the key ring is not a [PGPSecretKeyRing]. + */ + val secretKey: PGPSecretKey? = + when (keys) { + is PGPSecretKeyRing -> keys.secretKey!! + else -> null + } + + /** OpenPGP key version. */ + val version: Int = publicKey.version + + /** + * Return all [PGPPublicKeys][PGPPublicKey] of this key ring. The first key in the list being + * the primary key. Note that the list is unmodifiable. + * + * @return list of public keys + */ + val publicKeys: List = keys.publicKeys.asSequence().toList() + + /** All secret keys. If the key ring is a [PGPPublicKeyRing], then return an empty list. */ + val secretKeys: List = + when (keys) { + is PGPSecretKeyRing -> keys.secretKeys.asSequence().toList() + else -> listOf() + } + + /** List of valid public subkeys. */ + val validSubkeys: List = + keys.publicKeys.asSequence().filter { isKeyValidlyBound(it.keyID) }.toList() + + /** List of valid user-IDs. */ + val validUserIds: List = userIds.filter { isUserIdBound(it) } + + /** List of valid and expired user-IDs. */ + val validAndExpiredUserIds: List = + userIds.filter { + val certification = signatures.userIdCertifications[it] ?: return@filter false + val revocation = signatures.userIdRevocations[it] ?: return@filter true + return@filter !revocation.isHardRevocation && + certification.creationTime > revocation.creationTime + } + + /** List of email addresses that can be extracted from the user-IDs. */ + val emailAddresses: List = + userIds.mapNotNull { + PATTERN_EMAIL_FROM_USERID.matcher(it).let { m1 -> + if (m1.find()) m1.group(1) + else + PATTERN_EMAIL_EXPLICIT.matcher(it).let { m2 -> + if (m2.find()) m2.group(1) else null + } + } + } + + /** Newest direct-key self-signature on the primary key. */ + val latestDirectKeySelfSignature: PGPSignature? = signatures.primaryKeySelfSignature + + /** Newest primary-key revocation self-signature. */ + val revocationSelfSignature: PGPSignature? = signatures.primaryKeyRevocation + + /** Public-key encryption-algorithm of the primary key. */ + val algorithm: PublicKeyAlgorithm = PublicKeyAlgorithm.requireFromId(publicKey.algorithm) + + /** Creation date of the primary key. */ + val creationDate: Date = publicKey.creationTime!! + + /** Latest date at which the key was modified (either by adding a subkey or self-signature). */ + val lastModified: Date = getMostRecentSignature()?.creationTime ?: getLatestKeyCreationDate() + + /** True, if the underlying keyring is a [PGPSecretKeyRing]. */ + val isSecretKey: Boolean = keys is PGPSecretKeyRing + + /** True, if there are no encrypted secret keys. */ + val isFullyDecrypted: Boolean = + !isSecretKey || secretKeys.all { it.hasDummyS2K() || it.isDecrypted() } + + /** True, if there are only encrypted secret keys. */ + val isFullyEncrypted: Boolean = + isSecretKey && secretKeys.none { !it.hasDummyS2K() && it.isDecrypted() } + + /** List of public keys, whose secret key counterparts can be used to decrypt messages. */ + val decryptionSubkeys: List = + keys.publicKeys + .asSequence() + .filter { + if (it.keyID != keyId) { + if (signatures.subkeyBindings[it.keyID] == null) { + LOGGER.debug("Subkey ${it.keyID.openPgpKeyId()} has no binding signature.") + return@filter false + } + } + if (!it.isEncryptionKey) { + LOGGER.debug("(Sub-?)Key ${it.keyID.openPgpKeyId()} is not encryption-capable.") + return@filter false + } + return@filter true + } + .toList() + + /** Expiration date of the primary key. */ + val primaryKeyExpirationDate: Date? + get() { + val directKeyExpirationDate: Date? = + latestDirectKeySelfSignature?.let { getKeyExpirationTimeAsDate(it, publicKey) } + val possiblyExpiredPrimaryUserId = getPossiblyExpiredPrimaryUserId() + val primaryUserIdCertification = + possiblyExpiredPrimaryUserId?.let { getLatestUserIdCertification(it) } + val userIdExpirationDate: Date? = + primaryUserIdCertification?.let { getKeyExpirationTimeAsDate(it, publicKey) } + + if (latestDirectKeySelfSignature == null && primaryUserIdCertification == null) { + throw NoSuchElementException( + "No direct-key signature and no user-id signature found.") + } + if (directKeyExpirationDate != null && userIdExpirationDate == null) { + return directKeyExpirationDate + } + if (directKeyExpirationDate == null) { + return userIdExpirationDate + } + return if (directKeyExpirationDate < userIdExpirationDate) directKeyExpirationDate + else userIdExpirationDate + } + + /** List of all subkeys that can be used to sign a message. */ + val signingSubkeys: List = + validSubkeys.filter { getKeyFlagsOf(it.keyID).contains(KeyFlag.SIGN_DATA) } + + /** Whether the key is usable for encryption. */ + val isUsableForEncryption: Boolean = isUsableForEncryption(EncryptionPurpose.ANY) + + /** + * Whether the key is capable of signing messages. This field is also true, if the key contains + * a subkey that is capable of signing messages, but where the secret key is unavailable, e.g. + * because it was moved to a smart-card. + * + * To check for keys that are actually usable to sign messages, use [isUsableForSigning]. + */ + val isSigningCapable: Boolean = isKeyValidlyBound(keyId) && signingSubkeys.isNotEmpty() + + /** Whether the key is actually usable to sign messages. */ + val isUsableForSigning: Boolean = + isSigningCapable && signingSubkeys.any { isSecretKeyAvailable(it.keyID) } + + /** [HashAlgorithm] preferences of the primary user-ID or if absent, of the primary key. */ + val preferredHashAlgorithms: Set + get() = + primaryUserId?.let { getPreferredHashAlgorithms(it) } + ?: getPreferredHashAlgorithms(keyId) + + /** + * [SymmetricKeyAlgorithm] preferences of the primary user-ID or if absent of the primary key. + */ + val preferredSymmetricKeyAlgorithms: Set + get() = + primaryUserId?.let { getPreferredSymmetricKeyAlgorithms(it) } + ?: getPreferredSymmetricKeyAlgorithms(keyId) + + /** [CompressionAlgorithm] preferences of the primary user-ID or if absent, the primary key. */ + val preferredCompressionAlgorithms: Set + get() = + primaryUserId?.let { getPreferredCompressionAlgorithms(it) } + ?: getPreferredCompressionAlgorithms(keyId) + + /** + * Return the expiration date of the subkey with the provided fingerprint. + * + * @param fingerprint subkey fingerprint + * @return expiration date or null + */ + fun getSubkeyExpirationDate(fingerprint: OpenPgpFingerprint): Date? { + return getSubkeyExpirationDate(fingerprint.keyId) + } + + /** + * Return the expiration date of the subkey with the provided keyId. + * + * @param keyId subkey keyId + * @return expiration date + */ + fun getSubkeyExpirationDate(keyId: Long): Date? { + if (publicKey.keyID == keyId) return primaryKeyExpirationDate + val subkey = + getPublicKey(keyId) + ?: throw NoSuchElementException( + "No subkey with key-ID ${keyId.openPgpKeyId()} found.") + val bindingSig = + getCurrentSubkeyBindingSignature(keyId) + ?: throw AssertionError("Subkey has no valid binding signature.") + return bindingSig.getKeyExpirationDate(subkey.creationTime) + } + + /** + * Return the date after which the key can no longer be used to perform the given use-case, + * caused by expiration. + * + * @return expiration date for the given use-case + */ + fun getExpirationDateForUse(use: KeyFlag): Date? { + require(use != KeyFlag.SPLIT && use != KeyFlag.SHARED) { + "SPLIT and SHARED are not uses, but properties." + } + + val primaryKeyExpiration = primaryKeyExpirationDate + val keysWithFlag: List = getKeysWithKeyFlag(use) + if (keysWithFlag.isEmpty()) + throw NoSuchElementException("No key with the required key flag found.") + + var nonExpiring = false + val latestSubkeyExpiration = + keysWithFlag + .map { key -> + getSubkeyExpirationDate(key.keyID).also { if (it == null) nonExpiring = true } + } + .filterNotNull() + .maxByOrNull { it } + + if (nonExpiring) return primaryKeyExpiration + return if (primaryKeyExpiration == null) latestSubkeyExpiration + else if (latestSubkeyExpiration == null) primaryKeyExpiration + else minOf(primaryKeyExpiration, latestSubkeyExpiration) + } + + /** + * Return true, if the given user-ID is hard-revoked. + * + * @return true, if the given user-ID is hard-revoked. + */ + fun isHardRevoked(userId: CharSequence): Boolean { + return signatures.userIdRevocations[userId]?.isHardRevocation ?: false + } + + /** + * Return a list of all keys which carry the provided key flag in their signature. + * + * @param flag flag + * @return keys with flag + */ + fun getKeysWithKeyFlag(flag: KeyFlag): List = + publicKeys.filter { getKeyFlagsOf(it.keyID).contains(flag) } + + /** + * Return a list of all subkeys which can be used to encrypt a message for the given user-ID. + * + * @return encryption subkeys + */ + fun getEncryptionSubkeys( + userId: CharSequence?, + purpose: EncryptionPurpose + ): List { + if (userId != null && !isUserIdValid(userId)) { + throw UnboundUserIdException( + OpenPgpFingerprint.of(keys), + userId.toString(), + getLatestUserIdCertification(userId), + getUserIdRevocation(userId)) + } + return getEncryptionSubkeys(purpose) + } + + /** + * Return a list of all subkeys which can be used to encrypt a message, given the purpose. + * + * @return subkeys which can be used for encryption + */ + fun getEncryptionSubkeys(purpose: EncryptionPurpose): List { + primaryKeyExpirationDate?.let { + if (it < referenceDate) { + LOGGER.debug( + "Certificate is expired: Primary key is expired on ${DateUtil.formatUTCDate(it)}") + return listOf() + } + } + + return keys.publicKeys + .asSequence() + .filter { + if (!isKeyValidlyBound(it.keyID)) { + LOGGER.debug("(Sub?)-Key ${it.keyID.openPgpKeyId()} is not validly bound.") + return@filter false + } + + getSubkeyExpirationDate(it.keyID)?.let { exp -> + if (exp < referenceDate) { + LOGGER.debug( + "(Sub?)-Key ${it.keyID.openPgpKeyId()} is expired on ${DateUtil.formatUTCDate(exp)}.") + return@filter false + } + } + + if (!it.isEncryptionKey) { + LOGGER.debug( + "(Sub?)-Key ${it.keyID.openPgpKeyId()} algorithm is not capable of encryption.") + return@filter false + } + + val keyFlags = getKeyFlagsOf(it.keyID) + when (purpose) { + EncryptionPurpose.COMMUNICATIONS -> + return@filter keyFlags.contains(KeyFlag.ENCRYPT_COMMS) + EncryptionPurpose.STORAGE -> + return@filter keyFlags.contains(KeyFlag.ENCRYPT_STORAGE) + EncryptionPurpose.ANY -> + return@filter keyFlags.contains(KeyFlag.ENCRYPT_COMMS) || + keyFlags.contains(KeyFlag.ENCRYPT_STORAGE) + } + } + .toList() + } + + /** + * Return, whether the key is usable for encryption, given the purpose. + * + * @return true, if the key can be used to encrypt a message according to the + * encryption-purpose. + */ + fun isUsableForEncryption(purpose: EncryptionPurpose): Boolean { + return isKeyValidlyBound(keyId) && getEncryptionSubkeys(purpose).isNotEmpty() + } + + /** + * Return the primary user-ID, even if it is possibly expired. + * + * @return possibly expired primary user-ID + */ + fun getPossiblyExpiredPrimaryUserId(): String? = + primaryUserId + ?: userIds + .mapNotNull { userId -> getLatestUserIdCertification(userId)?.let { userId to it } } + .sortedByDescending { it.second.creationTime } + .maxByOrNull { it.second.hashedSubPackets.isPrimaryUserID } + ?.first + + /** Return the most-recently created self-signature on the key. */ + private fun getMostRecentSignature(): PGPSignature? = + setOfNotNull(latestDirectKeySelfSignature, revocationSelfSignature) + .asSequence() + .plus(signatures.userIdCertifications.values) + .plus(signatures.userIdRevocations.values) + .plus(signatures.subkeyBindings.values) + .plus(signatures.subkeyRevocations.values) + .maxByOrNull { it.creationTime } + /** + * Return the creation time of the latest added subkey. + * + * @return latest key creation time + */ + fun getLatestKeyCreationDate(): Date = + validSubkeys.maxByOrNull { creationDate }?.creationTime + ?: throw AssertionError("Apparently there is no validly bound key in this key ring.") + + /** + * Return the latest certification self-signature for the given user-ID. + * + * @return latest self-certification for the given user-ID. + */ + fun getLatestUserIdCertification(userId: CharSequence): PGPSignature? = + signatures.userIdCertifications[userId] + + /** + * Return the latest revocation self-signature for the given user-ID + * + * @return latest user-ID revocation for the given user-ID + */ + fun getUserIdRevocation(userId: CharSequence): PGPSignature? = + signatures.userIdRevocations[userId] + + /** + * Return the current binding signature for the subkey with the given key-ID. + * + * @return current subkey binding signature + */ + fun getCurrentSubkeyBindingSignature(keyId: Long): PGPSignature? = + signatures.subkeyBindings[keyId] + + /** + * Return the current revocation signature for the subkey with the given key-ID. + * + * @return current subkey revocation signature + */ + fun getSubkeyRevocationSignature(keyId: Long): PGPSignature? = + signatures.subkeyRevocations[keyId] + + /** + * Return a list of [KeyFlags][KeyFlag] that apply to the subkey with the provided key id. + * + * @param keyId key-id + * @return list of key flags + */ + fun getKeyFlagsOf(keyId: Long): List = + if (keyId == publicKey.keyID) { + latestDirectKeySelfSignature?.let { sig -> + SignatureSubpacketsUtil.parseKeyFlags(sig)?.let { flags -> + return flags + } + } + + primaryUserId?.let { + SignatureSubpacketsUtil.parseKeyFlags(getLatestUserIdCertification(it))?.let { flags + -> + return flags + } + } + listOf() + } else { + getCurrentSubkeyBindingSignature(keyId)?.let { + SignatureSubpacketsUtil.parseKeyFlags(it)?.let { flags -> + return flags + } + } + listOf() + } + + /** + * Return a list of [KeyFlags][KeyFlag] that apply to the given user-id. + * + * @param userId user-id + * @return key flags + */ + fun getKeyFlagsOf(userId: CharSequence): List = + if (!isUserIdValid(userId)) { + listOf() + } else { + getLatestUserIdCertification(userId)?.let { + SignatureSubpacketsUtil.parseKeyFlags(it) ?: listOf() + } + ?: throw AssertionError( + "While user-id '$userId' was reported as valid, there appears to be no certification for it.") + } + + /** + * Return the public key with the given key id from the provided key ring. + * + * @param keyId key id + * @return public key or null + */ + fun getPublicKey(keyId: Long): PGPPublicKey? = keys.getPublicKey(keyId) + + /** + * Return the secret key with the given key id. + * + * @param keyId key id + * @return secret key or null + */ + fun getSecretKey(keyId: Long): PGPSecretKey? = + when (keys) { + is PGPSecretKeyRing -> keys.getSecretKey(keyId) + else -> null + } + + /** + * Return true, if the secret-key with the given key-ID is available (i.e. not moved to a + * smart-card). + * + * @return availability of the secret key + */ + fun isSecretKeyAvailable(keyId: Long): Boolean { + return getSecretKey(keyId)?.let { + return if (it.s2K == null) true // Unencrypted key + else it.s2K.type !in 100..110 // Secret key on smart-card + } + ?: false // Missing secret key + } + + /** + * Return the public key with the given fingerprint. + * + * @param fingerprint fingerprint + * @return public key or null + */ + fun getPublicKey(fingerprint: OpenPgpFingerprint): PGPPublicKey? = + keys.getPublicKey(fingerprint.keyId) + + /** + * Return the secret key with the given fingerprint. + * + * @param fingerprint fingerprint + * @return secret key or null + */ + fun getSecretKey(fingerprint: OpenPgpFingerprint): PGPSecretKey? = + when (keys) { + is PGPSecretKeyRing -> keys.getSecretKey(fingerprint.keyId) + else -> null + } + + /** + * Return the public key matching the given [SubkeyIdentifier]. + * + * @return public key + * @throws IllegalArgumentException if the identifier's primary key does not match the primary + * key of the key. + */ + fun getPublicKey(identifier: SubkeyIdentifier): PGPPublicKey? { + require(identifier.primaryKeyId == publicKey.keyID) { "Mismatching primary key ID." } + return getPublicKey(identifier.subkeyId) + } + + /** + * Return the secret key matching the given [SubkeyIdentifier]. + * + * @return secret key + * @throws IllegalArgumentException if the identifier's primary key does not match the primary + * key of the key. + */ + fun getSecretKey(identifier: SubkeyIdentifier): PGPSecretKey? = + when (keys) { + is PGPSecretKeyRing -> { + require(identifier.primaryKeyId == publicKey.keyID) { + "Mismatching primary key ID." + } + keys.getSecretKey(identifier.subkeyId) + } + else -> null + } + + /** + * Return true if the public key with the given key id is bound to the key ring properly. + * + * @param keyId key id + * @return true if key is bound validly + */ + fun isKeyValidlyBound(keyId: Long): Boolean { + val publicKey = keys.getPublicKey(keyId) ?: return false + + // Primary key -> Check Primary Key Revocation + if (publicKey.keyID == this.publicKey.keyID) { + return if (signatures.primaryKeyRevocation != null && + signatures.primaryKeyRevocation.isHardRevocation) { + false + } else signatures.primaryKeyRevocation == null + } + + // Else Subkey -> Check Subkey Revocation + val binding = signatures.subkeyBindings[keyId] + val revocation = signatures.subkeyRevocations[keyId] + + // No valid binding + if (binding == null || binding.isExpired(referenceDate)) { + return false + } + + // Revocation + return if (revocation != null) { + if (revocation.isHardRevocation) { + // Subkey is hard revoked + false + } else { + // Key is soft-revoked, not yet re-bound + (revocation.isExpired(referenceDate) || + !revocation.creationTime.after(binding.creationTime)) + } + } else true + } + + /** + * Return the current primary user-id of the key ring. + * + *

+ * Note: If no user-id is marked as primary key using a [PrimaryUserID] packet, this method + * returns the first user-id on the key, otherwise null. + * + * @return primary user-id or null + */ + private fun findPrimaryUserId(): String? { + if (userIds.isEmpty()) { + return null + } + + return signatures.userIdCertifications + .filter { (_, certification) -> certification.hashedSubPackets.isPrimaryUserID } + .entries + .maxByOrNull { (_, certification) -> certification.creationTime } + ?.key + ?: signatures.userIdCertifications.keys.firstOrNull() + } + + /** Return true, if the primary user-ID, as well as the given user-ID are valid and bound. */ + fun isUserIdValid(userId: CharSequence) = + if (primaryUserId == null) { + false + } else { + isUserIdBound(primaryUserId) && + (if (userId == primaryUserId) true else isUserIdBound(userId)) + } + + /** Return true, if the given user-ID is validly bound. */ + fun isUserIdBound(userId: CharSequence) = + signatures.userIdCertifications[userId]?.let { sig -> + if (sig.isExpired(referenceDate)) { + // certification expired + return false + } + if (sig.hashedSubPackets.isPrimaryUserID) { + getKeyExpirationTimeAsDate(sig, publicKey)?.let { expirationDate -> + // key expired? + if (expirationDate < referenceDate) return false + } + } + signatures.userIdRevocations[userId]?.let { rev -> + if (rev.isHardRevocation) { + return false // hard revoked -> invalid + } + sig.creationTime > rev.creationTime // re-certification after soft revocation? + } + ?: true // certification, but no revocation + } + ?: false // no certification + + /** [HashAlgorithm] preferences of the given user-ID. */ + fun getPreferredHashAlgorithms(userId: CharSequence): Set { + return getKeyAccessor(userId, keyId).preferredHashAlgorithms + } + + /** [HashAlgorithm] preferences of the given key. */ + fun getPreferredHashAlgorithms(keyId: Long): Set { + return KeyAccessor.SubKey(this, SubkeyIdentifier(keys, keyId)).preferredHashAlgorithms + } + + /** [SymmetricKeyAlgorithm] preferences of the given user-ID. */ + fun getPreferredSymmetricKeyAlgorithms(userId: CharSequence): Set { + return getKeyAccessor(userId, keyId).preferredSymmetricKeyAlgorithms + } + + /** [SymmetricKeyAlgorithm] preferences of the given key. */ + fun getPreferredSymmetricKeyAlgorithms(keyId: Long): Set { + return KeyAccessor.SubKey(this, SubkeyIdentifier(keys, keyId)) + .preferredSymmetricKeyAlgorithms + } + + /** [CompressionAlgorithm] preferences of the given user-ID. */ + fun getPreferredCompressionAlgorithms(userId: CharSequence): Set { + return getKeyAccessor(userId, keyId).preferredCompressionAlgorithms + } + + /** [CompressionAlgorithm] preferences of the given key. */ + fun getPreferredCompressionAlgorithms(keyId: Long): Set { + return KeyAccessor.SubKey(this, SubkeyIdentifier(keys, keyId)) + .preferredCompressionAlgorithms + } + + val isUsableForThirdPartyCertification: Boolean = + isKeyValidlyBound(keyId) && getKeyFlagsOf(keyId).contains(KeyFlag.CERTIFY_OTHER) + + private fun getKeyAccessor(userId: CharSequence?, keyId: Long): KeyAccessor { + if (getPublicKey(keyId) == null) { + throw NoSuchElementException( + "No subkey with key-id ${keyId.openPgpKeyId()} found on this key.") + } + if (userId != null && !userIds.contains(userId)) { + throw NoSuchElementException("No user-id '$userId' found on this key.") + } + return if (userId != null) { + KeyAccessor.ViaUserId(this, SubkeyIdentifier(keys, keyId), userId) + } else { + KeyAccessor.ViaKeyId(this, SubkeyIdentifier(keys, keyId)) + } + } + + companion object { + + /** Evaluate the key for the given signature. */ + @JvmStatic + fun evaluateForSignature(keys: PGPKeyRing, signature: PGPSignature) = + KeyRingInfo(keys, signature.creationTime!!) + + private val PATTERN_EMAIL_FROM_USERID = + "<([a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+)>".toPattern() + private val PATTERN_EMAIL_EXPLICIT = + "^([a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+)$".toPattern() + + @JvmStatic private val LOGGER = LoggerFactory.getLogger(KeyRingInfo::class.java) + } + + private class Signatures(val keys: PGPKeyRing, val referenceDate: Date, val policy: Policy) { + val primaryKeyRevocation: PGPSignature? = + SignaturePicker.pickCurrentRevocationSelfSignature(keys, policy, referenceDate) + val primaryKeySelfSignature: PGPSignature? = + SignaturePicker.pickLatestDirectKeySignature(keys, policy, referenceDate) + val userIdRevocations = mutableMapOf() + val userIdCertifications = mutableMapOf() + val subkeyRevocations = mutableMapOf() + val subkeyBindings = mutableMapOf() + + init { + KeyRingUtils.getUserIdsIgnoringInvalidUTF8(keys.publicKey).forEach { userId -> + SignaturePicker.pickCurrentUserIdRevocationSignature( + keys, userId, policy, referenceDate) + ?.let { userIdRevocations[userId] = it } + SignaturePicker.pickLatestUserIdCertificationSignature( + keys, userId, policy, referenceDate) + ?.let { userIdCertifications[userId] = it } + } + keys.publicKeys.asSequence().drop(1).forEach { subkey -> + SignaturePicker.pickCurrentSubkeyBindingRevocationSignature( + keys, subkey, policy, referenceDate) + ?.let { subkeyRevocations[subkey.keyID] = it } + SignaturePicker.pickLatestSubkeyBindingSignature( + keys, subkey, policy, referenceDate) + ?.let { subkeyBindings[subkey.keyID] = it } + } + } + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.kt new file mode 100644 index 00000000..5480442d --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.kt @@ -0,0 +1,785 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.modification.secretkeyring + +import java.util.* +import java.util.function.Predicate +import javax.annotation.Nonnull +import kotlin.NoSuchElementException +import openpgp.openPgpKeyId +import org.bouncycastle.bcpg.sig.KeyExpirationTime +import org.bouncycastle.openpgp.* +import org.pgpainless.PGPainless +import org.pgpainless.PGPainless.Companion.inspectKeyRing +import org.pgpainless.algorithm.AlgorithmSuite +import org.pgpainless.algorithm.Feature +import org.pgpainless.algorithm.KeyFlag +import org.pgpainless.algorithm.SignatureType +import org.pgpainless.algorithm.negotiation.HashAlgorithmNegotiator +import org.pgpainless.bouncycastle.extensions.getKeyExpirationDate +import org.pgpainless.bouncycastle.extensions.publicKeyAlgorithm +import org.pgpainless.bouncycastle.extensions.requirePublicKey +import org.pgpainless.implementation.ImplementationFactory +import org.pgpainless.key.OpenPgpFingerprint +import org.pgpainless.key.generation.KeyRingBuilder +import org.pgpainless.key.generation.KeySpec +import org.pgpainless.key.protection.* +import org.pgpainless.key.protection.passphrase_provider.SolitaryPassphraseProvider +import org.pgpainless.key.util.KeyRingUtils +import org.pgpainless.key.util.KeyRingUtils.Companion.changePassphrase +import org.pgpainless.key.util.KeyRingUtils.Companion.injectCertification +import org.pgpainless.key.util.RevocationAttributes +import org.pgpainless.signature.builder.* +import org.pgpainless.signature.subpackets.* +import org.pgpainless.util.Passphrase +import org.pgpainless.util.selection.userid.SelectUserId + +class SecretKeyRingEditor( + var secretKeyRing: PGPSecretKeyRing, + override val referenceTime: Date = Date() +) : SecretKeyRingEditorInterface { + + override fun addUserId( + userId: CharSequence, + callback: SelfSignatureSubpackets.Callback?, + protector: SecretKeyRingProtector + ): SecretKeyRingEditorInterface { + val sanitizedUserId = sanitizeUserId(userId).toString() + val primaryKey = secretKeyRing.secretKey + + val info = inspectKeyRing(secretKeyRing, referenceTime) + require(!info.isHardRevoked(userId)) { + "User-ID $userId is hard revoked and cannot be re-certified." + } + + val ( + hashAlgorithmPreferences, + symmetricKeyAlgorithmPreferences, + compressionAlgorithmPreferences) = + try { + Triple( + info.preferredHashAlgorithms, + info.preferredSymmetricKeyAlgorithms, + info.preferredCompressionAlgorithms) + } catch (e: IllegalStateException) { // missing user-id sig + val algorithmSuite = AlgorithmSuite.defaultAlgorithmSuite + Triple( + algorithmSuite.hashAlgorithms, + algorithmSuite.symmetricKeyAlgorithms, + algorithmSuite.compressionAlgorithms) + } + + val builder = + SelfSignatureBuilder(primaryKey, protector).apply { + hashedSubpackets.setSignatureCreationTime(referenceTime) + setSignatureType(SignatureType.POSITIVE_CERTIFICATION) + } + builder.hashedSubpackets.apply { + setKeyFlags(info.getKeyFlagsOf(primaryKey.keyID)) + setPreferredHashAlgorithms(hashAlgorithmPreferences) + setPreferredSymmetricKeyAlgorithms(symmetricKeyAlgorithmPreferences) + setPreferredCompressionAlgorithms(compressionAlgorithmPreferences) + setFeatures(Feature.MODIFICATION_DETECTION) + } + builder.applyCallback(callback) + secretKeyRing = + injectCertification(secretKeyRing, sanitizedUserId, builder.build(sanitizedUserId)) + return this + } + + override fun addPrimaryUserId( + userId: CharSequence, + protector: SecretKeyRingProtector + ): SecretKeyRingEditorInterface { + val uid = sanitizeUserId(userId) + val primaryKey = secretKeyRing.publicKey + var info = inspectKeyRing(secretKeyRing, referenceTime) + val primaryUserId = info.primaryUserId + val signature = + if (primaryUserId == null) info.latestDirectKeySelfSignature + else info.getLatestUserIdCertification(primaryUserId) + val previousKeyExpiration = signature?.getKeyExpirationDate(primaryKey.creationTime) + + // Add new primary user-id signature + addUserId( + uid, + object : SelfSignatureSubpackets.Callback { + override fun modifyHashedSubpackets(hashedSubpackets: SelfSignatureSubpackets) { + hashedSubpackets.apply { + setPrimaryUserId() + if (previousKeyExpiration != null) + setKeyExpirationTime(primaryKey, previousKeyExpiration) + else setKeyExpirationTime(null) + } + } + }, + protector) + + // unmark previous primary user-ids to be non-primary + info = inspectKeyRing(secretKeyRing, referenceTime) + info.validAndExpiredUserIds + .filterNot { it == uid } + .forEach { otherUserId -> + if (info + .getLatestUserIdCertification(otherUserId)!! + .hashedSubPackets + .isPrimaryUserID) { + // We need to unmark this user-id as primary + addUserId( + otherUserId, + object : SelfSignatureSubpackets.Callback { + override fun modifyHashedSubpackets( + hashedSubpackets: SelfSignatureSubpackets + ) { + hashedSubpackets.apply { + setPrimaryUserId(null) + setKeyExpirationTime(null) // non-primary + } + } + }, + protector) + } + } + return this + } + + @Deprecated( + "Use of SelectUserId class is deprecated.", + replaceWith = ReplaceWith("removeUserId(protector, predicate)")) + override fun removeUserId( + selector: SelectUserId, + protector: SecretKeyRingProtector + ): SecretKeyRingEditorInterface { + return revokeUserIds( + selector, + protector, + RevocationAttributes.createCertificateRevocation() + .withReason(RevocationAttributes.Reason.USER_ID_NO_LONGER_VALID) + .withoutDescription()) + } + + override fun removeUserId( + protector: SecretKeyRingProtector, + predicate: (String) -> Boolean + ): SecretKeyRingEditorInterface { + return revokeUserIds( + protector, + RevocationAttributes.createCertificateRevocation() + .withReason(RevocationAttributes.Reason.USER_ID_NO_LONGER_VALID) + .withoutDescription(), + predicate) + } + + override fun removeUserId( + userId: CharSequence, + protector: SecretKeyRingProtector + ): SecretKeyRingEditorInterface { + return removeUserId(protector) { uid -> userId == uid } + } + + override fun replaceUserId( + oldUserId: CharSequence, + newUserId: CharSequence, + protector: SecretKeyRingProtector + ): SecretKeyRingEditorInterface { + val oldUID = sanitizeUserId(oldUserId) + val newUID = sanitizeUserId(newUserId) + require(oldUID.isNotBlank()) { "Old user-ID cannot be empty." } + require(newUID.isNotBlank()) { "New user-ID cannot be empty." } + + val info = inspectKeyRing(secretKeyRing, referenceTime) + if (!info.isUserIdValid(oldUID)) { + throw NoSuchElementException( + "Key does not carry user-ID '$oldUID', or it is not valid.") + } + + val oldCertification = + info.getLatestUserIdCertification(oldUID) + ?: throw AssertionError("Certification for old user-ID MUST NOT be null.") + + addUserId( + newUID, + object : SelfSignatureSubpackets.Callback { + override fun modifyHashedSubpackets(hashedSubpackets: SelfSignatureSubpackets) { + SignatureSubpacketsHelper.applyFrom( + oldCertification.hashedSubPackets, hashedSubpackets as SignatureSubpackets) + if (oldUID == info.primaryUserId && + !oldCertification.hashedSubPackets.isPrimaryUserID) { + hashedSubpackets.setPrimaryUserId() + } + } + + override fun modifyUnhashedSubpackets(unhashedSubpackets: SelfSignatureSubpackets) { + SignatureSubpacketsHelper.applyFrom( + oldCertification.unhashedSubPackets, + unhashedSubpackets as SignatureSubpackets) + } + }, + protector) + + return revokeUserId(oldUID, protector) + } + + override fun addSubKey( + keySpec: KeySpec, + subkeyPassphrase: Passphrase, + protector: SecretKeyRingProtector + ): SecretKeyRingEditorInterface { + val callback = + object : SelfSignatureSubpackets.Callback { + override fun modifyHashedSubpackets(hashedSubpackets: SelfSignatureSubpackets) { + SignatureSubpacketsHelper.applyFrom( + keySpec.subpackets, hashedSubpackets as SignatureSubpackets) + hashedSubpackets.setSignatureCreationTime(referenceTime) + } + } + return addSubKey(keySpec, subkeyPassphrase, callback, protector) + } + + override fun addSubKey( + keySpec: KeySpec, + subkeyPassphrase: Passphrase, + callback: SelfSignatureSubpackets.Callback?, + protector: SecretKeyRingProtector + ): SecretKeyRingEditorInterface { + val keyPair = KeyRingBuilder.generateKeyPair(keySpec, referenceTime) + val subkeyProtector = + PasswordBasedSecretKeyRingProtector.forKeyId(keyPair.keyID, subkeyPassphrase) + val keyFlags = KeyFlag.fromBitmask(keySpec.subpackets.keyFlags).toMutableList() + return addSubKey( + keyPair, + callback, + subkeyProtector, + protector, + keyFlags.removeFirst(), + *keyFlags.toTypedArray()) + } + + override fun addSubKey( + subkey: PGPKeyPair, + callback: SelfSignatureSubpackets.Callback?, + subkeyProtector: SecretKeyRingProtector, + primaryKeyProtector: SecretKeyRingProtector, + keyFlag: KeyFlag, + vararg keyFlags: KeyFlag + ): SecretKeyRingEditorInterface { + val flags = listOf(keyFlag).plus(keyFlags) + val subkeyAlgorithm = subkey.publicKey.publicKeyAlgorithm + SignatureSubpacketsUtil.assureKeyCanCarryFlags(subkeyAlgorithm) + + val bitStrength = subkey.publicKey.bitStrength + require( + PGPainless.getPolicy() + .publicKeyAlgorithmPolicy + .isAcceptable(subkeyAlgorithm, bitStrength)) { + "Public key algorithm policy violation: $subkeyAlgorithm with bit strength $bitStrength is not acceptable." + } + + val primaryKey = secretKeyRing.secretKey + val info = inspectKeyRing(secretKeyRing, referenceTime) + val hashAlgorithm = + HashAlgorithmNegotiator.negotiateSignatureHashAlgorithm(PGPainless.getPolicy()) + .negotiateHashAlgorithm(info.preferredHashAlgorithms) + + var secretSubkey = + PGPSecretKey( + subkey.privateKey, + subkey.publicKey, + ImplementationFactory.getInstance().v4FingerprintCalculator, + false, + subkeyProtector.getEncryptor(subkey.keyID)) + val skBindingBuilder = + SubkeyBindingSignatureBuilder(primaryKey, primaryKeyProtector, hashAlgorithm) + skBindingBuilder.apply { + hashedSubpackets.setSignatureCreationTime(referenceTime) + hashedSubpackets.setKeyFlags(flags) + if (subkeyAlgorithm.isSigningCapable()) { + val pkBindingBuilder = + PrimaryKeyBindingSignatureBuilder(secretSubkey, subkeyProtector, hashAlgorithm) + pkBindingBuilder.hashedSubpackets.setSignatureCreationTime(referenceTime) + hashedSubpackets.addEmbeddedSignature(pkBindingBuilder.build(primaryKey.publicKey)) + } + applyCallback(callback) + } + secretSubkey = + KeyRingUtils.secretKeyPlusSignature( + secretSubkey, skBindingBuilder.build(secretSubkey.publicKey)) + secretKeyRing = KeyRingUtils.keysPlusSecretKey(secretKeyRing, secretSubkey) + return this + } + + override fun revoke( + protector: SecretKeyRingProtector, + revocationAttributes: RevocationAttributes? + ): SecretKeyRingEditorInterface { + return revoke(protector, callbackFromRevocationAttributes(revocationAttributes)) + } + + override fun revoke( + protector: SecretKeyRingProtector, + callback: RevocationSignatureSubpackets.Callback? + ): SecretKeyRingEditorInterface { + return revokeSubKey(secretKeyRing.secretKey.keyID, protector, callback) + } + + override fun revokeSubKey( + subkeyId: Long, + protector: SecretKeyRingProtector, + revocationAttributes: RevocationAttributes? + ): SecretKeyRingEditorInterface { + return revokeSubKey( + subkeyId, protector, callbackFromRevocationAttributes(revocationAttributes)) + } + + override fun revokeSubKey( + subkeyId: Long, + protector: SecretKeyRingProtector, + callback: RevocationSignatureSubpackets.Callback? + ): SecretKeyRingEditorInterface { + val revokeeSubKey = secretKeyRing.requirePublicKey(subkeyId) + val subkeyRevocation = generateRevocation(protector, revokeeSubKey, callback) + secretKeyRing = injectCertification(secretKeyRing, revokeeSubKey, subkeyRevocation) + return this + } + + override fun revokeUserId( + userId: CharSequence, + protector: SecretKeyRingProtector, + revocationAttributes: RevocationAttributes? + ): SecretKeyRingEditorInterface { + if (revocationAttributes != null) { + require( + revocationAttributes.reason == RevocationAttributes.Reason.NO_REASON || + revocationAttributes.reason == + RevocationAttributes.Reason.USER_ID_NO_LONGER_VALID) { + "Revocation reason must either be NO_REASON or USER_ID_NO_LONGER_VALID" + } + } + + return revokeUserId( + userId, + protector, + object : RevocationSignatureSubpackets.Callback { + override fun modifyHashedSubpackets( + hashedSubpackets: RevocationSignatureSubpackets + ) { + if (revocationAttributes != null) { + hashedSubpackets.setRevocationReason(false, revocationAttributes) + } + } + }) + } + + override fun revokeUserId( + userId: CharSequence, + protector: SecretKeyRingProtector, + callback: RevocationSignatureSubpackets.Callback? + ): SecretKeyRingEditorInterface { + return revokeUserIds(protector, callback, SelectUserId.exactMatch(sanitizeUserId(userId))) + } + + override fun revokeUserIds( + protector: SecretKeyRingProtector, + revocationAttributes: RevocationAttributes?, + predicate: (String) -> Boolean + ): SecretKeyRingEditorInterface { + return revokeUserIds( + protector, + object : RevocationSignatureSubpackets.Callback { + override fun modifyHashedSubpackets( + hashedSubpackets: RevocationSignatureSubpackets + ) { + if (revocationAttributes != null) + hashedSubpackets.setRevocationReason(revocationAttributes) + } + }, + predicate) + } + + override fun revokeUserIds( + protector: SecretKeyRingProtector, + callback: RevocationSignatureSubpackets.Callback?, + predicate: (String) -> Boolean + ): SecretKeyRingEditorInterface { + selectUserIds(predicate) + .also { + if (it.isEmpty()) + throw NoSuchElementException("No matching user-ids found on the key.") + } + .forEach { userId -> doRevokeUserId(userId, protector, callback) } + return this + } + + override fun setExpirationDate( + expiration: Date?, + protector: SecretKeyRingProtector + ): SecretKeyRingEditorInterface { + require(secretKeyRing.secretKey.isMasterKey) { + "OpenPGP key does not appear to contain a primary secret key." + } + + val prevDirectKeySig = getPreviousDirectKeySignature() + // reissue direct key sig + if (prevDirectKeySig != null) { + secretKeyRing = + injectCertification( + secretKeyRing, + secretKeyRing.publicKey, + reissueDirectKeySignature(expiration, protector, prevDirectKeySig)) + } + + val primaryUserId = + inspectKeyRing(secretKeyRing, referenceTime).getPossiblyExpiredPrimaryUserId() + if (primaryUserId != null) { + val prevUserIdSig = getPreviousUserIdSignatures(primaryUserId) + val userIdSig = + reissuePrimaryUserIdSig(expiration, protector, primaryUserId, prevUserIdSig!!) + secretKeyRing = injectCertification(secretKeyRing, primaryUserId, userIdSig) + } + + val info = inspectKeyRing(secretKeyRing, referenceTime) + for (userId in info.validUserIds) { + if (userId == primaryUserId) { + continue + } + + val prevUserIdSig = + info.getLatestUserIdCertification(userId) + ?: throw AssertionError( + "A valid user-id shall never have no user-id signature.") + if (prevUserIdSig.hashedSubPackets.isPrimaryUserID) { + secretKeyRing = + injectCertification( + secretKeyRing, + primaryUserId!!, + reissueNonPrimaryUserId(protector, userId, prevUserIdSig)) + } + } + + return this + } + + override fun setExpirationDateOfSubkey( + expiration: Date?, + keyId: Long, + protector: SecretKeyRingProtector + ): SecretKeyRingEditorInterface = apply { + // is primary key + if (keyId == secretKeyRing.publicKey.keyID) { + return setExpirationDate(expiration, protector) + } + + // is subkey + val subkey = + secretKeyRing.getPublicKey(keyId) + ?: throw NoSuchElementException("No subkey with ID ${keyId.openPgpKeyId()} found.") + val prevBinding = + inspectKeyRing(secretKeyRing).getCurrentSubkeyBindingSignature(keyId) + ?: throw NoSuchElementException( + "Previous subkey binding signature for ${keyId.openPgpKeyId()} MUST NOT be null.") + val bindingSig = reissueSubkeyBindingSignature(subkey, expiration, protector, prevBinding) + secretKeyRing = injectCertification(secretKeyRing, subkey, bindingSig) + } + + override fun createMinimalRevocationCertificate( + protector: SecretKeyRingProtector, + revocationAttributes: RevocationAttributes? + ): PGPPublicKeyRing { + // Check reason + if (revocationAttributes != null) { + require(RevocationAttributes.Reason.isKeyRevocation(revocationAttributes.reason)) { + "Revocation reason MUST be applicable to a key revocation." + } + } + + val revocation = createRevocation(protector, revocationAttributes) + var primaryKey = secretKeyRing.secretKey.publicKey + primaryKey = KeyRingUtils.getStrippedDownPublicKey(primaryKey) + primaryKey = PGPPublicKey.addCertification(primaryKey, revocation) + return PGPPublicKeyRing(listOf(primaryKey)) + } + + override fun createRevocation( + protector: SecretKeyRingProtector, + revocationAttributes: RevocationAttributes? + ): PGPSignature { + return generateRevocation( + protector, + secretKeyRing.publicKey, + callbackFromRevocationAttributes(revocationAttributes)) + } + + override fun createRevocation( + subkeyId: Long, + protector: SecretKeyRingProtector, + revocationAttributes: RevocationAttributes? + ): PGPSignature { + return generateRevocation( + protector, + secretKeyRing.requirePublicKey(subkeyId), + callbackFromRevocationAttributes(revocationAttributes)) + } + + override fun createRevocation( + subkeyId: Long, + protector: SecretKeyRingProtector, + callback: RevocationSignatureSubpackets.Callback? + ): PGPSignature { + return generateRevocation(protector, secretKeyRing.requirePublicKey(subkeyId), callback) + } + + override fun createRevocation( + subkeyFingerprint: OpenPgpFingerprint, + protector: SecretKeyRingProtector, + revocationAttributes: RevocationAttributes? + ): PGPSignature { + return generateRevocation( + protector, + secretKeyRing.requirePublicKey(subkeyFingerprint), + callbackFromRevocationAttributes(revocationAttributes)) + } + + override fun changePassphraseFromOldPassphrase( + oldPassphrase: Passphrase, + oldProtectionSettings: KeyRingProtectionSettings + ): SecretKeyRingEditorInterface.WithKeyRingEncryptionSettings { + return WithKeyRingEncryptionSettingsImpl( + this, + null, + PasswordBasedSecretKeyRingProtector( + oldProtectionSettings, SolitaryPassphraseProvider(oldPassphrase))) + } + + override fun changeSubKeyPassphraseFromOldPassphrase( + keyId: Long, + oldPassphrase: Passphrase, + oldProtectionSettings: KeyRingProtectionSettings + ): SecretKeyRingEditorInterface.WithKeyRingEncryptionSettings { + return WithKeyRingEncryptionSettingsImpl( + this, + keyId, + CachingSecretKeyRingProtector( + mapOf(keyId to oldPassphrase), oldProtectionSettings, null)) + } + + override fun done(): PGPSecretKeyRing { + return secretKeyRing + } + + private fun sanitizeUserId(userId: CharSequence): CharSequence = + // I'm not sure, what kind of sanitization is needed. + // Newlines are allowed, they just need to be escaped when emitted in an ASCII armor header + // Trailing/Leading whitespace is also fine. + userId.toString() + + private fun callbackFromRevocationAttributes(attributes: RevocationAttributes?) = + object : RevocationSignatureSubpackets.Callback { + override fun modifyHashedSubpackets(hashedSubpackets: RevocationSignatureSubpackets) { + if (attributes != null) { + hashedSubpackets.setRevocationReason(attributes) + } + } + } + + private fun generateRevocation( + protector: SecretKeyRingProtector, + revokeeSubkey: PGPPublicKey, + callback: RevocationSignatureSubpackets.Callback? + ): PGPSignature { + val primaryKey = secretKeyRing.secretKey + val signatureType = + if (revokeeSubkey.isMasterKey) SignatureType.KEY_REVOCATION + else SignatureType.SUBKEY_REVOCATION + + return RevocationSignatureBuilder(signatureType, primaryKey, protector) + .apply { applyCallback(callback) } + .build(revokeeSubkey) + } + + private fun doRevokeUserId( + userId: CharSequence, + protector: SecretKeyRingProtector, + callback: RevocationSignatureSubpackets.Callback? + ): SecretKeyRingEditorInterface { + RevocationSignatureBuilder( + SignatureType.CERTIFICATION_REVOCATION, secretKeyRing.secretKey, protector) + .apply { + hashedSubpackets.setSignatureCreationTime(referenceTime) + applyCallback(callback) + } + .let { + secretKeyRing = + injectCertification(secretKeyRing, userId, it.build(userId.toString())) + } + return this + } + + private fun getPreviousDirectKeySignature(): PGPSignature? { + val info = inspectKeyRing(secretKeyRing, referenceTime) + return info.latestDirectKeySelfSignature + } + + private fun getPreviousUserIdSignatures(userId: String): PGPSignature? { + val info = inspectKeyRing(secretKeyRing, referenceTime) + return info.getLatestUserIdCertification(userId) + } + + @Throws(PGPException::class) + private fun reissueNonPrimaryUserId( + secretKeyRingProtector: SecretKeyRingProtector, + userId: String, + prevUserIdSig: PGPSignature + ): PGPSignature { + val builder = + SelfSignatureBuilder(secretKeyRing.secretKey, secretKeyRingProtector, prevUserIdSig) + builder.hashedSubpackets.setSignatureCreationTime(referenceTime) + builder.applyCallback( + object : SelfSignatureSubpackets.Callback { + override fun modifyHashedSubpackets(hashedSubpackets: SelfSignatureSubpackets) { + // unmark as primary + hashedSubpackets.setPrimaryUserId(null) + } + }) + return builder.build(userId) + } + + @Throws(PGPException::class) + private fun reissuePrimaryUserIdSig( + expiration: Date?, + @Nonnull secretKeyRingProtector: SecretKeyRingProtector, + @Nonnull primaryUserId: String, + @Nonnull prevUserIdSig: PGPSignature + ): PGPSignature { + return SelfSignatureBuilder(secretKeyRing.secretKey, secretKeyRingProtector, prevUserIdSig) + .apply { + hashedSubpackets.setSignatureCreationTime(referenceTime) + applyCallback( + object : SelfSignatureSubpackets.Callback { + override fun modifyHashedSubpackets( + hashedSubpackets: SelfSignatureSubpackets + ) { + if (expiration != null) { + hashedSubpackets.setKeyExpirationTime( + true, secretKeyRing.publicKey.creationTime, expiration) + } else { + hashedSubpackets.setKeyExpirationTime(KeyExpirationTime(true, 0)) + } + hashedSubpackets.setPrimaryUserId() + } + }) + } + .build(primaryUserId) + } + + @Throws(PGPException::class) + private fun reissueDirectKeySignature( + expiration: Date?, + secretKeyRingProtector: SecretKeyRingProtector, + prevDirectKeySig: PGPSignature + ): PGPSignature { + return DirectKeySelfSignatureBuilder( + secretKeyRing.secretKey, secretKeyRingProtector, prevDirectKeySig) + .apply { + hashedSubpackets.setSignatureCreationTime(referenceTime) + applyCallback( + object : SelfSignatureSubpackets.Callback { + override fun modifyHashedSubpackets( + hashedSubpackets: SelfSignatureSubpackets + ) { + if (expiration != null) { + hashedSubpackets.setKeyExpirationTime( + secretKeyRing.publicKey.creationTime, expiration) + } else { + hashedSubpackets.setKeyExpirationTime(null) + } + } + }) + } + .build() + } + + private fun reissueSubkeyBindingSignature( + subkey: PGPPublicKey, + expiration: Date?, + protector: SecretKeyRingProtector, + prevSubkeyBindingSignature: PGPSignature + ): PGPSignature { + val primaryKey = secretKeyRing.publicKey + val secretPrimaryKey = secretKeyRing.secretKey + val secretSubkey: PGPSecretKey? = secretKeyRing.getSecretKey(subkey.keyID) + + val builder = + SubkeyBindingSignatureBuilder(secretPrimaryKey, protector, prevSubkeyBindingSignature) + builder.hashedSubpackets.apply { + // set expiration + setSignatureCreationTime(referenceTime) + setKeyExpirationTime(subkey, expiration) + setSignatureExpirationTime(null) // avoid copying sig exp time + + // signing-capable subkeys need embedded primary key binding sig + SignatureSubpacketsUtil.parseKeyFlags(prevSubkeyBindingSignature)?.let { flags -> + if (flags.contains(KeyFlag.SIGN_DATA)) { + if (secretSubkey == null) { + throw NoSuchElementException( + "Secret key does not contain secret-key" + + " component for subkey ${subkey.keyID.openPgpKeyId()}") + } + + // create new embedded back-sig + clearEmbeddedSignatures() + addEmbeddedSignature( + PrimaryKeyBindingSignatureBuilder(secretSubkey, protector) + .build(primaryKey)) + } + } + } + + return builder.build(subkey) + } + + private fun selectUserIds(predicate: Predicate): List = + inspectKeyRing(secretKeyRing).validUserIds.filter { predicate.test(it) } + + private class WithKeyRingEncryptionSettingsImpl( + private val editor: SecretKeyRingEditor, + private val keyId: Long?, + private val oldProtector: SecretKeyRingProtector + ) : SecretKeyRingEditorInterface.WithKeyRingEncryptionSettings { + + override fun withSecureDefaultSettings(): SecretKeyRingEditorInterface.WithPassphrase { + return withCustomSettings(KeyRingProtectionSettings.secureDefaultSettings()) + } + + override fun withCustomSettings( + settings: KeyRingProtectionSettings + ): SecretKeyRingEditorInterface.WithPassphrase { + return WithPassphraseImpl(editor, keyId, oldProtector, settings) + } + } + + private class WithPassphraseImpl( + private val editor: SecretKeyRingEditor, + private val keyId: Long?, + private val oldProtector: SecretKeyRingProtector, + private val newProtectionSettings: KeyRingProtectionSettings + ) : SecretKeyRingEditorInterface.WithPassphrase { + + override fun toNewPassphrase(passphrase: Passphrase): SecretKeyRingEditorInterface { + val protector = + PasswordBasedSecretKeyRingProtector( + newProtectionSettings, SolitaryPassphraseProvider(passphrase)) + val secretKeys = changePassphrase(keyId, editor.secretKeyRing, oldProtector, protector) + editor.secretKeyRing = secretKeys + return editor + } + + override fun toNoPassphrase(): SecretKeyRingEditorInterface { + val protector = UnprotectedKeysProtector() + val secretKeys = changePassphrase(keyId, editor.secretKeyRing, oldProtector, protector) + editor.secretKeyRing = secretKeys + return editor + } + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.kt new file mode 100644 index 00000000..140ff905 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.kt @@ -0,0 +1,678 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.modification.secretkeyring + +import java.io.IOException +import java.security.InvalidAlgorithmParameterException +import java.security.NoSuchAlgorithmException +import java.util.* +import org.bouncycastle.openpgp.* +import org.pgpainless.algorithm.KeyFlag +import org.pgpainless.key.OpenPgpFingerprint +import org.pgpainless.key.generation.KeySpec +import org.pgpainless.key.protection.KeyRingProtectionSettings +import org.pgpainless.key.protection.SecretKeyRingProtector +import org.pgpainless.key.util.RevocationAttributes +import org.pgpainless.signature.subpackets.RevocationSignatureSubpackets +import org.pgpainless.signature.subpackets.SelfSignatureSubpackets +import org.pgpainless.util.Passphrase +import org.pgpainless.util.selection.userid.SelectUserId + +interface SecretKeyRingEditorInterface { + + /** + * Editors reference time. This time is used as creation date for new signatures, or as + * reference when evaluating expiration of existing signatures. + */ + val referenceTime: Date + + /** + * Add a user-id to the key ring. + * + * @param userId user-id + * @param protector protector to unlock the secret key + * @return the builder + * @throws PGPException in case we cannot generate a signature for the user-id + */ + @Throws(PGPException::class) + fun addUserId(userId: CharSequence, protector: SecretKeyRingProtector) = + addUserId(userId, null, protector) + + /** + * Add a user-id to the key ring. + * + * @param userId user-id + * @param callback callback to modify the self-signature subpackets + * @param protector protector to unlock the secret key + * @return the builder + * @throws PGPException in case we cannot generate a signature for the user-id + */ + @Throws(PGPException::class) + fun addUserId( + userId: CharSequence, + callback: SelfSignatureSubpackets.Callback? = null, + protector: SecretKeyRingProtector + ): SecretKeyRingEditorInterface + + /** + * Add a user-id to the key ring and mark it as primary. If the user-id is already present, a + * new certification signature will be created. + * + * @param userId user id + * @param protector protector to unlock the secret key + * @return the builder + * @throws PGPException in case we cannot generate a signature for the user-id + */ + @Throws(PGPException::class) + fun addPrimaryUserId( + userId: CharSequence, + protector: SecretKeyRingProtector + ): SecretKeyRingEditorInterface + + /** + * Convenience method to revoke selected user-ids using soft revocation signatures. The + * revocation will use [RevocationAttributes.Reason.USER_ID_NO_LONGER_VALID], so that the + * user-id can be re-certified at a later point. + * + * @param selector selector to select user-ids + * @param protector protector to unlock the primary key + * @return the builder + * @throws PGPException in case we cannot generate a revocation signature for the user-id + */ + @Deprecated( + "Use of SelectUserId class is deprecated.", + ReplaceWith("removeUserId(protector, predicate)")) + @Throws(PGPException::class) + fun removeUserId(selector: SelectUserId, protector: SecretKeyRingProtector) = + removeUserId(protector, selector) + + /** + * Convenience method to revoke selected user-ids using soft revocation signatures. The + * revocation will use [RevocationAttributes.Reason.USER_ID_NO_LONGER_VALID], so that the + * user-id can be re-certified at a later point. + * + * @param protector protector to unlock the primary key + * @param predicate predicate to select user-ids for revocation + * @return the builder + * @throws PGPException in case we cannot generate a revocation signature for the user-id + */ + @Throws(PGPException::class) + fun removeUserId( + protector: SecretKeyRingProtector, + predicate: (String) -> Boolean + ): SecretKeyRingEditorInterface + + /** + * Convenience method to revoke a single user-id using a soft revocation signature. The + * revocation will use [RevocationAttributes.Reason.USER_ID_NO_LONGER_VALID] so that the user-id + * can be re-certified at a later point. + * + * @param userId user-id to revoke + * @param protector protector to unlock the primary key + * @return the builder + * @throws PGPException in case we cannot generate a revocation signature for the user-id + */ + @Throws(PGPException::class) + fun removeUserId( + userId: CharSequence, + protector: SecretKeyRingProtector + ): SecretKeyRingEditorInterface + + /** + * Replace a user-id on the key with a new one. The old user-id gets soft revoked and the new + * user-id gets bound with the same signature subpackets as the old one, with one exception: If + * the old user-id was implicitly primary (did not carry a + * [org.bouncycastle.bcpg.sig.PrimaryUserID] packet, but effectively was primary), then the new + * user-id will be explicitly marked as primary. + * + * @param oldUserId old user-id + * @param newUserId new user-id + * @param protector protector to unlock the secret key + * @return the builder + * @throws PGPException in case we cannot generate a revocation and certification signature + * @throws java.util.NoSuchElementException if the old user-id was not found on the key; or if + * the oldUserId was already invalid + */ + @Throws(PGPException::class) + fun replaceUserId( + oldUserId: CharSequence, + newUserId: CharSequence, + protector: SecretKeyRingProtector + ): SecretKeyRingEditorInterface + + /** + * Add a subkey to the key ring. The subkey will be generated from the provided [KeySpec]. + * + * @param keySpec key specification + * @param subkeyPassphrase passphrase to encrypt the sub key + * @param callback callback to modify the subpackets of the subkey binding signature + * @param protector protector to unlock the secret key of the key ring + * @return the builder + * @throws InvalidAlgorithmParameterException in case the user wants to use invalid parameters + * for the key + * @throws NoSuchAlgorithmException in case of missing algorithm support in the crypto backend + * @throws PGPException in case we cannot generate a binding signature for the subkey + * @throws IOException in case of an IO error + */ + @Throws( + PGPException::class, + IOException::class, + InvalidAlgorithmParameterException::class, + NoSuchAlgorithmException::class) + fun addSubKey( + keySpec: KeySpec, + subkeyPassphrase: Passphrase, + callback: SelfSignatureSubpackets.Callback? = null, + protector: SecretKeyRingProtector + ): SecretKeyRingEditorInterface + + /** + * Add a subkey to the key ring. + * + * @param subkey subkey key pair + * @param callback callback to modify the subpackets of the subkey binding signature + * @param subkeyProtector protector to unlock and encrypt the subkey + * @param primaryKeyProtector protector to unlock the primary key + * @param keyFlag first mandatory key flag for the subkey + * @param keyFlags optional additional key flags + * @return builder + * @throws PGPException in case we cannot generate a binding signature for the subkey + * @throws IOException in case of an IO error + */ + @Throws(PGPException::class, IOException::class) + fun addSubKey( + subkey: PGPKeyPair, + callback: SelfSignatureSubpackets.Callback?, + subkeyProtector: SecretKeyRingProtector, + primaryKeyProtector: SecretKeyRingProtector, + keyFlag: KeyFlag, + vararg keyFlags: KeyFlag + ): SecretKeyRingEditorInterface + + /** + * Revoke the key ring using a hard revocation. + * + * @param protector protector of the primary key + * @return the builder + * @throws PGPException in case we cannot generate a revocation signature + */ + @Throws(PGPException::class) + fun revoke(protector: SecretKeyRingProtector) = revoke(protector, null as RevocationAttributes?) + + /** + * Revoke the key ring using the provided revocation attributes. The attributes define, whether + * the revocation was a hard revocation or not. + * + * @param protector protector of the primary key + * @param revocationAttributes reason for the revocation + * @return the builder + * @throws PGPException in case we cannot generate a revocation signature + */ + @Throws(PGPException::class) + fun revoke( + protector: SecretKeyRingProtector, + revocationAttributes: RevocationAttributes? = null + ): SecretKeyRingEditorInterface + + /** + * Revoke the key ring. You can use the [RevocationSignatureSubpackets.Callback] to modify the + * revocation signatures subpackets, e.g. in order to define whether this is a hard or soft + * revocation. + * + * @param protector protector to unlock the primary secret key + * @param callback callback to modify the revocations subpackets + * @return builder + * @throws PGPException in case we cannot generate a revocation signature + */ + @Throws(PGPException::class) + fun revoke( + protector: SecretKeyRingProtector, + callback: RevocationSignatureSubpackets.Callback? + ): SecretKeyRingEditorInterface + + /** + * Revoke the subkey binding signature of a subkey. The subkey with the provided fingerprint + * will be revoked. If no suitable subkey is found, a [NoSuchElementException] will be thrown. + * + * @param fingerprint fingerprint of the subkey to be revoked + * @param protector protector to unlock the primary key + * @return the builder + * @throws PGPException in case we cannot generate a revocation signature for the subkey + */ + @Throws(PGPException::class) + fun revokeSubKey(fingerprint: OpenPgpFingerprint, protector: SecretKeyRingProtector) = + revokeSubKey(fingerprint, protector, null) + + /** + * Revoke the subkey binding signature of a subkey. The subkey with the provided fingerprint + * will be revoked. If no suitable subkey is found, a [NoSuchElementException] will be thrown. + * + * @param fingerprint fingerprint of the subkey to be revoked + * @param protector protector to unlock the primary key + * @param revocationAttributes reason for the revocation + * @return the builder + * @throws PGPException in case we cannot generate a revocation signature for the subkey + */ + @Throws(PGPException::class) + fun revokeSubKey( + fingerprint: OpenPgpFingerprint, + protector: SecretKeyRingProtector, + revocationAttributes: RevocationAttributes? = null + ): SecretKeyRingEditorInterface = + revokeSubKey(fingerprint.keyId, protector, revocationAttributes) + + /** + * Revoke the subkey binding signature of a subkey. The subkey with the provided key-id will be + * revoked. If no suitable subkey is found, a [NoSuchElementException] will be thrown. + * + * @param subkeyId id of the subkey + * @param protector protector to unlock the primary key + * @return the builder + * @throws PGPException in case we cannot generate a revocation signature for the subkey + */ + @Throws(PGPException::class) + fun revokeSubKey(subkeyId: Long, protector: SecretKeyRingProtector) = + revokeSubKey(subkeyId, protector, null as RevocationAttributes?) + + /** + * Revoke the subkey binding signature of a subkey. The subkey with the provided key-id will be + * revoked. If no suitable subkey is found, a [NoSuchElementException] will be thrown. + * + * @param subkeyId id of the subkey + * @param protector protector to unlock the primary key + * @param revocationAttributes reason for the revocation + * @return the builder + * @throws PGPException in case we cannot generate a revocation signature for the subkey + */ + @Throws(PGPException::class) + fun revokeSubKey( + subkeyId: Long, + protector: SecretKeyRingProtector, + revocationAttributes: RevocationAttributes? = null + ): SecretKeyRingEditorInterface + + /** + * Revoke the subkey binding signature of a subkey. The subkey with the provided key-id will be + * revoked. If no suitable subkey is found, a [NoSuchElementException] will be thrown. + * + * The provided subpackets callback is used to modify the revocation signatures subpackets. + * + * @param subkeyId id of the subkey + * @param protector protector to unlock the secret key ring + * @param callback callback which can be used to modify the subpackets of the revocation + * signature + * @return the builder + * @throws PGPException in case we cannot generate a revocation signature for the subkey + */ + @Throws(PGPException::class) + fun revokeSubKey( + subkeyId: Long, + protector: SecretKeyRingProtector, + callback: RevocationSignatureSubpackets.Callback? + ): SecretKeyRingEditorInterface + + /** + * Hard-revoke the given userID. + * + * @param userId userId to revoke + * @param protector protector to unlock the primary key + * @return the builder + * @throws PGPException in case we cannot generate a revocation signature for the user-id + */ + @Throws(PGPException::class) + fun revokeUserId(userId: CharSequence, protector: SecretKeyRingProtector) = + revokeUserId(userId, protector, null as RevocationAttributes?) + + /** + * Revoke the given userID using the provided revocation attributes. + * + * @param userId userId to revoke + * @param protector protector to unlock the primary key + * @param revocationAttributes reason for the revocation + * @return the builder + * @throws PGPException in case we cannot generate a revocation signature for the user-id + */ + @Throws(PGPException::class) + fun revokeUserId( + userId: CharSequence, + protector: SecretKeyRingProtector, + revocationAttributes: RevocationAttributes? = null + ): SecretKeyRingEditorInterface + + /** + * Revoke the provided user-id. Note: If you don't provide a + * [RevocationSignatureSubpackets.Callback] which sets a revocation reason + * ([RevocationAttributes]), the revocation will be considered hard. So if you intend to + * re-certify the user-id at a later point to make it valid again, make sure to set a soft + * revocation reason in the signatures hashed area using the subpacket callback. + * + * @param userId userid to be revoked + * @param protector protector to unlock the primary secret key + * @param callback callback to modify the revocations subpackets + * @return builder + * @throws PGPException in case we cannot generate a revocation signature for the user-id + */ + @Throws(PGPException::class) + fun revokeUserId( + userId: CharSequence, + protector: SecretKeyRingProtector, + callback: RevocationSignatureSubpackets.Callback? + ): SecretKeyRingEditorInterface + + /** + * Revoke all user-ids that match the provided [SelectUserId] filter. The provided + * [RevocationAttributes] will be set as reason for revocation in each revocation signature. + * + * Note: If you intend to re-certify these user-ids at a later point, make sure to choose a soft + * revocation reason. See [RevocationAttributes.Reason] for more information. + * + * @param selector user-id selector + * @param protector protector to unlock the primary secret key + * @param revocationAttributes revocation attributes + * @return builder + * @throws PGPException in case we cannot generate a revocation signature for the user-id + */ + @Throws(PGPException::class) + @Deprecated( + "Use of SelectUserId class is deprecated.", + ReplaceWith("revokeUserIds(protector, revocationAttributes, predicate)")) + fun revokeUserIds( + selector: SelectUserId, + protector: SecretKeyRingProtector, + revocationAttributes: RevocationAttributes? + ) = revokeUserIds(protector, revocationAttributes, selector) + + /** + * Revoke all user-ids that match the provided [SelectUserId] filter. The provided + * [RevocationAttributes] will be set as reason for revocation in each revocation signature. + * + * Note: If you intend to re-certify these user-ids at a later point, make sure to choose a soft + * revocation reason. See [RevocationAttributes.Reason] for more information. + * + * @param protector protector to unlock the primary secret key + * @param revocationAttributes revocation attributes + * @param predicate to select user-ids for revocation + * @return builder + * @throws PGPException in case we cannot generate a revocation signature for the user-id + */ + @Throws(PGPException::class) + fun revokeUserIds( + protector: SecretKeyRingProtector, + revocationAttributes: RevocationAttributes?, + predicate: (String) -> Boolean + ): SecretKeyRingEditorInterface + + /** + * Revoke all user-ids that match the provided [SelectUserId] filter. The provided + * [RevocationSignatureSubpackets.Callback] will be used to modify the revocation signatures + * subpackets. + * + * Note: If you intend to re-certify these user-ids at a later point, make sure to set a soft + * revocation reason in the revocation signatures hashed subpacket area using the callback. + * + * See [RevocationAttributes.Reason] for more information. + * + * @param selector user-id selector + * @param protector protector to unlock the primary secret key + * @param callback callback to modify the revocations subpackets + * @return builder + * @throws PGPException in case we cannot generate a revocation signature for the user-id + */ + @Throws(PGPException::class) + @Deprecated( + "Use of SelectUserId class is deprecated.", + ReplaceWith("revokeUserIds(protector, callback, predicate)")) + fun revokeUserIds( + selector: SelectUserId, + protector: SecretKeyRingProtector, + callback: RevocationSignatureSubpackets.Callback? + ) = revokeUserIds(protector, callback, selector) + + /** + * Revoke all user-ids that match the provided [SelectUserId] filter. The provided + * [RevocationSignatureSubpackets.Callback] will be used to modify the revocation signatures + * subpackets. + * + * Note: If you intend to re-certify these user-ids at a later point, make sure to set a soft + * revocation reason in the revocation signatures hashed subpacket area using the callback. + * + * See [RevocationAttributes.Reason] for more information. + * + * @param protector protector to unlock the primary secret key + * @param callback callback to modify the revocations subpackets + * @param predicate to select user-ids for revocation + * @return builder + * @throws PGPException in case we cannot generate a revocation signature for the user-id + */ + @Throws(PGPException::class) + fun revokeUserIds( + protector: SecretKeyRingProtector, + callback: RevocationSignatureSubpackets.Callback?, + predicate: (String) -> Boolean + ): SecretKeyRingEditorInterface + + /** + * Set the expiration date for the primary key of the key ring. If the key is supposed to never + * expire, then an expiration date of null is expected. + * + * @param expiration new expiration date or null + * @param protector to unlock the secret key + * @return the builder + * @throws PGPException in case we cannot generate a new self-signature with the changed + * expiration date + */ + @Throws(PGPException::class) + fun setExpirationDate( + expiration: Date?, + protector: SecretKeyRingProtector + ): SecretKeyRingEditorInterface + + /** + * Set the expiration date for the subkey identified by the given keyId to the given expiration + * date. If the key is supposed to never expire, then an expiration date of null is expected. + * + * @param expiration new expiration date of null + * @param keyId id of the subkey + * @param protector to unlock the secret key + * @return the builder + * @throws PGPException in case we cannot generate a new subkey-binding or self-signature with + * the changed expiration date + */ + @Throws(PGPException::class) + fun setExpirationDateOfSubkey( + expiration: Date?, + keyId: Long, + protector: SecretKeyRingProtector + ): SecretKeyRingEditorInterface + + /** + * Create a minimal, self-authorizing revocation certificate, containing only the primary key + * and a revocation signature. This type of revocation certificates was introduced in OpenPGP + * v6. This method has no side effects on the original key and will leave it intact. + * + * @param protector protector to unlock the primary key. + * @param revocationAttributes reason for the revocation (key revocation) + * @return minimal revocation certificate + * @throws PGPException in case we cannot generate a revocation signature + */ + @Throws(PGPException::class) + fun createMinimalRevocationCertificate( + protector: SecretKeyRingProtector, + revocationAttributes: RevocationAttributes? + ): PGPPublicKeyRing + + /** + * Create a detached revocation certificate, which can be used to revoke the whole key. The + * original key will not be modified by this method. + * + * @param protector protector to unlock the primary key. + * @param revocationAttributes reason for the revocation + * @return revocation certificate + * @throws PGPException in case we cannot generate a revocation certificate + */ + @Throws(PGPException::class) + fun createRevocation( + protector: SecretKeyRingProtector, + revocationAttributes: RevocationAttributes? + ): PGPSignature + + /** + * Create a detached revocation certificate, which can be used to revoke the specified subkey. + * The original key will not be modified by this method. + * + * @param subkeyId id of the subkey to be revoked + * @param protector protector to unlock the primary key. + * @param revocationAttributes reason for the revocation + * @return revocation certificate + * @throws PGPException in case we cannot generate a revocation certificate + */ + @Throws(PGPException::class) + fun createRevocation( + subkeyId: Long, + protector: SecretKeyRingProtector, + revocationAttributes: RevocationAttributes? + ): PGPSignature + + /** + * Create a detached revocation certificate, which can be used to revoke the specified subkey. + * The original key will not be modified by this method. + * + * @param subkeyId id of the subkey to be revoked + * @param protector protector to unlock the primary key. + * @param callback callback to modify the subpackets of the revocation certificate. + * @return revocation certificate + * @throws PGPException in case we cannot generate a revocation certificate + */ + @Throws(PGPException::class) + fun createRevocation( + subkeyId: Long, + protector: SecretKeyRingProtector, + callback: RevocationSignatureSubpackets.Callback? + ): PGPSignature + + /** + * Create a detached revocation certificate, which can be used to revoke the specified subkey. + * The original key will not be modified by this method. + * + * @param subkeyFingerprint fingerprint of the subkey to be revoked + * @param protector protector to unlock the primary key. + * @param revocationAttributes reason for the revocation + * @return revocation certificate + * @throws PGPException in case we cannot generate a revocation certificate + */ + @Throws(PGPException::class) + fun createRevocation( + subkeyFingerprint: OpenPgpFingerprint, + protector: SecretKeyRingProtector, + revocationAttributes: RevocationAttributes? + ): PGPSignature + + /** + * Change the passphrase of the whole key ring. + * + * @param oldPassphrase old passphrase (empty, if the key was unprotected) + * @return next builder step + */ + fun changePassphraseFromOldPassphrase(oldPassphrase: Passphrase) = + changePassphraseFromOldPassphrase( + oldPassphrase, KeyRingProtectionSettings.secureDefaultSettings()) + + /** + * Change the passphrase of the whole key ring. + * + * @param oldPassphrase old passphrase (empty, if the key was unprotected) + * @param oldProtectionSettings custom settings for the old passphrase + * @return next builder step + */ + fun changePassphraseFromOldPassphrase( + oldPassphrase: Passphrase, + oldProtectionSettings: KeyRingProtectionSettings = + KeyRingProtectionSettings.secureDefaultSettings() + ): WithKeyRingEncryptionSettings + + /** + * Change the passphrase of a single subkey in the key ring. + * + * Note: While it is a valid use-case to have different passphrases per subKey, this is one of + * the reasons why OpenPGP sucks in practice. + * + * @param keyId id of the subkey + * @param oldPassphrase old passphrase (empty if the key was unprotected) + * @return next builder step + */ + fun changeSubKeyPassphraseFromOldPassphrase(keyId: Long, oldPassphrase: Passphrase) = + changeSubKeyPassphraseFromOldPassphrase( + keyId, oldPassphrase, KeyRingProtectionSettings.secureDefaultSettings()) + + /** + * Change the passphrase of a single subkey in the key ring. + * + * Note: While it is a valid use-case to have different passphrases per subKey, this is one of + * the reasons why OpenPGP sucks in practice. + * + * @param keyId id of the subkey + * @param oldPassphrase old passphrase (empty if the key was unprotected) + * @param oldProtectionSettings custom settings for the old passphrase + * @return next builder step + */ + fun changeSubKeyPassphraseFromOldPassphrase( + keyId: Long, + oldPassphrase: Passphrase, + oldProtectionSettings: KeyRingProtectionSettings + ): WithKeyRingEncryptionSettings + + interface WithKeyRingEncryptionSettings { + + /** + * Set secure default settings for the symmetric passphrase encryption. Note that this + * obviously has no effect if you decide to set [WithPassphrase.toNoPassphrase]. + * + * @return next builder step + */ + fun withSecureDefaultSettings(): WithPassphrase + + /** + * Set custom settings for the symmetric passphrase encryption. + * + * @param settings custom settings + * @return next builder step + */ + fun withCustomSettings(settings: KeyRingProtectionSettings): WithPassphrase + } + + interface WithPassphrase { + + /** + * Set the passphrase. + * + * @param passphrase passphrase + * @return editor builder + * @throws PGPException in case the passphrase cannot be changed + */ + @Throws(PGPException::class) + fun toNewPassphrase(passphrase: Passphrase): SecretKeyRingEditorInterface + + /** + * Leave the key unprotected. + * + * @return editor builder + * @throws PGPException in case the passphrase cannot be changed + */ + @Throws(PGPException::class) fun toNoPassphrase(): SecretKeyRingEditorInterface + } + + /** + * Return the [PGPSecretKeyRing]. + * + * @return the key + */ + fun done(): PGPSecretKeyRing + + fun addSubKey( + keySpec: KeySpec, + subkeyPassphrase: Passphrase, + protector: SecretKeyRingProtector + ): SecretKeyRingEditorInterface +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/parsing/KeyRingReader.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/parsing/KeyRingReader.kt new file mode 100644 index 00000000..6f6bde61 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/parsing/KeyRingReader.kt @@ -0,0 +1,337 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.parsing + +import java.io.IOException +import java.io.InputStream +import java.nio.charset.Charset +import kotlin.jvm.Throws +import org.bouncycastle.openpgp.* +import org.bouncycastle.util.io.Streams +import org.pgpainless.PGPainless +import org.pgpainless.implementation.ImplementationFactory +import org.pgpainless.key.collection.PGPKeyRingCollection +import org.pgpainless.util.ArmorUtils + +class KeyRingReader { + + /** + * Read a [PGPKeyRing] (either [PGPSecretKeyRing] or [PGPPublicKeyRing]) from the given + * [InputStream]. + * + * @param inputStream inputStream containing the OpenPGP key or certificate + * @return key ring + * @throws IOException in case of an IO error + */ + @Throws(IOException::class) + fun keyRing(inputStream: InputStream): PGPKeyRing? = readKeyRing(inputStream) + + /** + * Read a [PGPKeyRing] (either [PGPSecretKeyRing] or [PGPPublicKeyRing]) from the given byte + * array. + * + * @param bytes byte array containing the OpenPGP key or certificate + * @return key ring + * @throws IOException in case of an IO error + */ + @Throws(IOException::class) + fun keyRing(bytes: ByteArray): PGPKeyRing? = keyRing(bytes.inputStream()) + + /** + * Read a [PGPKeyRing] (either [PGPSecretKeyRing] or [PGPPublicKeyRing]) from the given ASCII + * armored string. + * + * @param asciiArmored ASCII armored OpenPGP key or certificate + * @return key ring + * @throws IOException in case of an IO error + */ + @Throws(IOException::class) + fun keyRing(asciiArmored: String): PGPKeyRing? = keyRing(asciiArmored.toByteArray(UTF8)) + + @Throws(IOException::class) + fun publicKeyRing(inputStream: InputStream): PGPPublicKeyRing? = readPublicKeyRing(inputStream) + + @Throws(IOException::class) + fun publicKeyRing(bytes: ByteArray): PGPPublicKeyRing? = publicKeyRing(bytes.inputStream()) + + @Throws(IOException::class) + fun publicKeyRing(asciiArmored: String): PGPPublicKeyRing? = + publicKeyRing(asciiArmored.toByteArray(UTF8)) + + @Throws(IOException::class) + fun publicKeyRingCollection(inputStream: InputStream): PGPPublicKeyRingCollection = + readPublicKeyRingCollection(inputStream) + + @Throws(IOException::class) + fun publicKeyRingCollection(bytes: ByteArray): PGPPublicKeyRingCollection = + publicKeyRingCollection(bytes.inputStream()) + + @Throws(IOException::class) + fun publicKeyRingCollection(asciiArmored: String): PGPPublicKeyRingCollection = + publicKeyRingCollection(asciiArmored.toByteArray(UTF8)) + + @Throws(IOException::class) + fun secretKeyRing(inputStream: InputStream): PGPSecretKeyRing? = readSecretKeyRing(inputStream) + + @Throws(IOException::class) + fun secretKeyRing(bytes: ByteArray): PGPSecretKeyRing? = secretKeyRing(bytes.inputStream()) + + @Throws(IOException::class) + fun secretKeyRing(asciiArmored: String): PGPSecretKeyRing? = + secretKeyRing(asciiArmored.toByteArray(UTF8)) + + @Throws(IOException::class) + fun secretKeyRingCollection(inputStream: InputStream): PGPSecretKeyRingCollection = + readSecretKeyRingCollection(inputStream) + + @Throws(IOException::class) + fun secretKeyRingCollection(bytes: ByteArray): PGPSecretKeyRingCollection = + secretKeyRingCollection(bytes.inputStream()) + + @Throws(IOException::class) + fun secretKeyRingCollection(asciiArmored: String): PGPSecretKeyRingCollection = + secretKeyRingCollection(asciiArmored.toByteArray(UTF8)) + + @Throws(IOException::class) + fun keyRingCollection(inptStream: InputStream, isSilent: Boolean): PGPKeyRingCollection = + readKeyRingCollection(inptStream, isSilent) + + @Throws(IOException::class) + fun keyRingCollection(bytes: ByteArray, isSilent: Boolean): PGPKeyRingCollection = + keyRingCollection(bytes.inputStream(), isSilent) + + @Throws(IOException::class) + fun keyRingCollection(asciiArmored: String, isSilent: Boolean): PGPKeyRingCollection = + keyRingCollection(asciiArmored.toByteArray(UTF8), isSilent) + + companion object { + private const val MAX_ITERATIONS = 10000 + + @JvmStatic val UTF8: Charset = charset("UTF8") + + /** + * Read a [PGPKeyRing] (either [PGPSecretKeyRing] or [PGPPublicKeyRing]) from the given + * [InputStream]. This method will attempt to read at most

maxIterations
objects + * from the stream before aborting. The first [PGPPublicKeyRing] or [PGPSecretKeyRing] will + * be returned. + * + * @param inputStream inputStream containing the OpenPGP key or certificate + * @param maxIterations maximum number of objects that are read before the method will abort + * @return key ring + * @throws IOException in case of an IO error + */ + @JvmStatic + @JvmOverloads + @Throws(IOException::class) + fun readKeyRing( + inputStream: InputStream, + maxIterations: Int = MAX_ITERATIONS + ): PGPKeyRing? { + val objectFactory = + ImplementationFactory.getInstance() + .getPGPObjectFactory(ArmorUtils.getDecoderStream(inputStream)) + + try { + for ((i, next) in objectFactory.withIndex()) { + if (i >= maxIterations) { + throw IOException("Loop exceeded max iteration count.") + } + if (next is PGPMarker) { + continue + } + if (next is PGPSecretKeyRing) { + return next + } + if (next is PGPPublicKeyRing) { + return next + } + continue + } + } catch (e: PGPRuntimeOperationException) { + throw e.cause!! + } + return null + } + + /** + * Read a public key ring from the provided [InputStream]. If more than maxIterations PGP + * packets are encountered before a [PGPPublicKeyRing] is read, an [IOException] is thrown. + * + * @param inputStream input stream + * @param maxIterations max iterations before abort + * @return public key ring + * @throws IOException in case of an IO error or exceeding of max iterations + */ + @JvmStatic + @JvmOverloads + @Throws(IOException::class) + fun readPublicKeyRing( + inputStream: InputStream, + maxIterations: Int = MAX_ITERATIONS + ): PGPPublicKeyRing? { + val objectFactory = + ImplementationFactory.getInstance() + .getPGPObjectFactory(ArmorUtils.getDecoderStream(inputStream)) + + try { + for ((i, next) in objectFactory.withIndex()) { + if (i >= maxIterations) { + throw IOException("Loop exceeded max iteration count.") + } + if (next is PGPMarker) { + continue + } + if (next is PGPPublicKeyRing) { + return next + } + continue + } + } catch (e: PGPRuntimeOperationException) { + throw e.cause!! + } + return null + } + + /** + * Read a public key ring collection from the provided [InputStream]. If more than + * maxIterations PGP packets are encountered before the stream is exhausted, an + * [IOException] is thrown. If the stream contain secret key packets, their public key parts + * are extracted and returned. + * + * @param inputStream input stream + * @param maxIterations max iterations before abort + * @return public key ring collection + * @throws IOException in case of an IO error or exceeding of max iterations + */ + @JvmStatic + @JvmOverloads + @Throws(IOException::class) + fun readPublicKeyRingCollection( + inputStream: InputStream, + maxIterations: Int = MAX_ITERATIONS + ): PGPPublicKeyRingCollection { + val objectFactory = + ImplementationFactory.getInstance() + .getPGPObjectFactory(ArmorUtils.getDecoderStream(inputStream)) + val certificates = mutableListOf() + try { + for ((i, next) in objectFactory.withIndex()) { + if (i >= maxIterations) { + throw IOException("Loop exceeded max iteration count.") + } + if (next is PGPMarker) { + continue + } + if (next is PGPPublicKeyRing) { + certificates.add(next) + continue + } + if (next is PGPSecretKeyRing) { + certificates.add(PGPainless.extractCertificate(next)) + continue + } + if (next is PGPPublicKeyRingCollection) { + certificates.addAll(next) + continue + } + } + } catch (e: PGPRuntimeOperationException) { + throw e.cause!! + } + return PGPPublicKeyRingCollection(certificates) + } + + /** + * Read a secret key ring from the provided [InputStream]. If more than maxIterations PGP + * packets are encountered before a [PGPSecretKeyRing] is read, an [IOException] is thrown. + * + * @param inputStream input stream + * @param maxIterations max iterations before abort + * @return public key ring + * @throws IOException in case of an IO error or exceeding of max iterations + */ + @JvmStatic + @JvmOverloads + @Throws(IOException::class) + fun readSecretKeyRing( + inputStream: InputStream, + maxIterations: Int = MAX_ITERATIONS + ): PGPSecretKeyRing? { + val decoderStream = ArmorUtils.getDecoderStream(inputStream) + val objectFactory = + ImplementationFactory.getInstance().getPGPObjectFactory(decoderStream) + + try { + for ((i, next) in objectFactory.withIndex()) { + if (i >= maxIterations) { + throw IOException("Loop exceeded max iteration count.") + } + if (next is PGPMarker) { + continue + } + if (next is PGPSecretKeyRing) { + Streams.drain(decoderStream) + return next + } + } + } catch (e: PGPRuntimeOperationException) { + throw e.cause!! + } + return null + } + + /** + * Read a secret key ring collection from the provided [InputStream]. If more than + * maxIterations PGP packets are encountered before the stream is exhausted, an + * [IOException] is thrown. + * + * @param inputStream input stream + * @param maxIterations max iterations before abort + * @return secret key ring collection + * @throws IOException in case of an IO error or exceeding of max iterations + */ + @JvmStatic + @JvmOverloads + @Throws(IOException::class) + fun readSecretKeyRingCollection( + inputStream: InputStream, + maxIterations: Int = MAX_ITERATIONS + ): PGPSecretKeyRingCollection { + val objectFactory = + ImplementationFactory.getInstance() + .getPGPObjectFactory(ArmorUtils.getDecoderStream(inputStream)) + + val secretKeys = mutableListOf() + try { + for ((i, next) in objectFactory.withIndex()) { + if (i >= maxIterations) { + throw IOException("Loop exceeded max iteration count.") + } + if (next is PGPMarker) { + continue + } + if (next is PGPSecretKeyRing) { + secretKeys.add(next) + continue + } + if (next is PGPSecretKeyRingCollection) { + secretKeys.addAll(next) + continue + } + } + } catch (e: PGPRuntimeOperationException) { + throw e.cause!! + } + return PGPSecretKeyRingCollection(secretKeys) + } + + @JvmStatic + @Throws(IOException::class) + fun readKeyRingCollection( + inputStream: InputStream, + isSilent: Boolean + ): PGPKeyRingCollection = PGPKeyRingCollection(inputStream, isSilent) + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/BaseSecretKeyRingProtector.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/BaseSecretKeyRingProtector.kt new file mode 100644 index 00000000..c5db2086 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/BaseSecretKeyRingProtector.kt @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.protection + +import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor +import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor +import org.pgpainless.implementation.ImplementationFactory +import org.pgpainless.key.protection.passphrase_provider.SecretKeyPassphraseProvider + +/** + * Basic [SecretKeyRingProtector] implementation that respects the users [KeyRingProtectionSettings] + * when encrypting keys. + */ +open class BaseSecretKeyRingProtector( + private val passphraseProvider: SecretKeyPassphraseProvider, + private val protectionSettings: KeyRingProtectionSettings +) : SecretKeyRingProtector { + + constructor( + passphraseProvider: SecretKeyPassphraseProvider + ) : this(passphraseProvider, KeyRingProtectionSettings.secureDefaultSettings()) + + override fun hasPassphraseFor(keyId: Long): Boolean = passphraseProvider.hasPassphrase(keyId) + + override fun getDecryptor(keyId: Long): PBESecretKeyDecryptor? = + passphraseProvider.getPassphraseFor(keyId)?.let { + if (it.isEmpty) null + else ImplementationFactory.getInstance().getPBESecretKeyDecryptor(it) + } + + override fun getEncryptor(keyId: Long): PBESecretKeyEncryptor? = + passphraseProvider.getPassphraseFor(keyId)?.let { + if (it.isEmpty) null + else + ImplementationFactory.getInstance() + .getPBESecretKeyEncryptor( + protectionSettings.encryptionAlgorithm, + protectionSettings.hashAlgorithm, + protectionSettings.s2kCount, + it) + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/CachingSecretKeyRingProtector.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/CachingSecretKeyRingProtector.kt new file mode 100644 index 00000000..20704685 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/CachingSecretKeyRingProtector.kt @@ -0,0 +1,167 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.protection + +import openpgp.openPgpKeyId +import org.bouncycastle.openpgp.PGPKeyRing +import org.bouncycastle.openpgp.PGPPublicKey +import org.pgpainless.key.OpenPgpFingerprint +import org.pgpainless.key.protection.passphrase_provider.SecretKeyPassphraseProvider +import org.pgpainless.util.Passphrase + +/** + * Implementation of the [SecretKeyRingProtector] which holds a map of key ids and their passwords. + * In case the needed passphrase is not contained in the map, the `missingPassphraseCallback` will + * be consulted, and the passphrase is added to the map. + * + * If you need to unlock multiple [PGPKeyRing] instances, it is advised to use a separate + * [CachingSecretKeyRingProtector] instance for each ring. + */ +class CachingSecretKeyRingProtector : SecretKeyRingProtector, SecretKeyPassphraseProvider { + + private val cache: MutableMap + private val protector: SecretKeyRingProtector + private val provider: SecretKeyPassphraseProvider? + + constructor() : this(null) + + constructor( + missingPassphraseCallback: SecretKeyPassphraseProvider? + ) : this( + mapOf(), + KeyRingProtectionSettings.secureDefaultSettings(), + missingPassphraseCallback) + + constructor( + passphrases: Map, + protectionSettings: KeyRingProtectionSettings, + missingPassphraseCallback: SecretKeyPassphraseProvider? + ) { + this.cache = passphrases.toMutableMap() + this.protector = PasswordBasedSecretKeyRingProtector(protectionSettings, this) + this.provider = missingPassphraseCallback + } + + /** + * Add a passphrase to the cache. If the cache already contains a passphrase for the given + * key-id, a [IllegalArgumentException] is thrown. The reason for this is to prevent accidental + * override of passphrases when dealing with multiple key rings containing a key with the same + * key-id but different passphrases. + * + * If you can ensure that there will be no key-id clash, and you want to replace the passphrase, + * you can use [replacePassphrase] to replace the passphrase. + * + * @param keyId id of the key + * @param passphrase passphrase + */ + fun addPassphrase(keyId: Long, passphrase: Passphrase) = apply { + require(!cache.containsKey(keyId)) { + "The cache already holds a passphrase for ID ${keyId.openPgpKeyId()}.\n" + + "If you want to replace this passphrase, use replacePassphrase(Long, Passphrase) instead." + } + cache[keyId] = passphrase + } + + /** + * Replace the passphrase for the given key-id in the cache. + * + * @param keyId keyId + * @param passphrase passphrase + */ + fun replacePassphrase(keyId: Long, passphrase: Passphrase) = apply { cache[keyId] = passphrase } + + /** + * Remember the given passphrase for all keys in the given key ring. If for the key-id of any + * key on the key ring the cache already contains a passphrase, a [IllegalArgumentException] is + * thrown before any changes are committed to the cache. This is to prevent accidental + * passphrase override when dealing with multiple key rings containing keys with conflicting + * key-ids. + * + * If you can ensure that there will be no key-id clashes, and you want to replace the + * passphrases for the key ring, use [replacePassphrase] instead. + * + * If you need to unlock multiple [PGPKeyRing], it is advised to use a separate + * [CachingSecretKeyRingProtector] instance for each ring. + * + * @param keyRing key ring + * @param passphrase passphrase + */ + fun addPassphrase(keyRing: PGPKeyRing, passphrase: Passphrase) = apply { + // check for existing passphrases before doing anything + keyRing.publicKeys.forEach { + require(!cache.containsKey(it.keyID)) { + "The cache already holds a passphrase for the key with ID ${it.keyID.openPgpKeyId()}.\n" + + "If you want to replace the passphrase, use replacePassphrase(PGPKeyRing, Passphrase) instead." + } + } + + // only then instert + keyRing.publicKeys.forEach { cache[it.keyID] = passphrase } + } + + /** + * Replace the cached passphrases for all keys in the key ring with the provided passphrase. + * + * @param keyRing key ring + * @param passphrase passphrase + */ + fun replacePassphrase(keyRing: PGPKeyRing, passphrase: Passphrase) = apply { + keyRing.publicKeys.forEach { cache[it.keyID] = passphrase } + } + + /** + * Remember the given passphrase for the given (sub-)key. + * + * @param key key + * @param passphrase passphrase + */ + fun addPassphrase(key: PGPPublicKey, passphrase: Passphrase) = + addPassphrase(key.keyID, passphrase) + + /** + * Remember the given passphrase for the key with the given fingerprint. + * + * @param fingerprint fingerprint + * @param passphrase passphrase + */ + fun addPassphrase(fingerprint: OpenPgpFingerprint, passphrase: Passphrase) = + addPassphrase(fingerprint.keyId, passphrase) + + /** + * Remove a passphrase from the cache. The passphrase will be cleared and then removed. + * + * @param keyId id of the key + */ + fun forgetPassphrase(keyId: Long) = apply { cache.remove(keyId)?.clear() } + + /** + * Forget the passphrase to all keys in the provided key ring. + * + * @param keyRing key ring + */ + fun forgetPassphrase(keyRing: PGPKeyRing) = apply { + keyRing.publicKeys.forEach { forgetPassphrase(it) } + } + + /** + * Forget the passphrase of the given public key. + * + * @param key key + */ + fun forgetPassphrase(key: PGPPublicKey) = apply { forgetPassphrase(key.keyID) } + + override fun getPassphraseFor(keyId: Long?): Passphrase? { + return if (hasPassphrase(keyId)) cache[keyId] + else provider?.getPassphraseFor(keyId)?.also { cache[keyId] = it } + } + + override fun hasPassphrase(keyId: Long?) = cache[keyId]?.isValid ?: false + + override fun hasPassphraseFor(keyId: Long) = hasPassphrase(keyId) + + override fun getDecryptor(keyId: Long) = protector.getDecryptor(keyId) + + override fun getEncryptor(keyId: Long) = protector.getEncryptor(keyId) +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/KeyRingProtectionSettings.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/KeyRingProtectionSettings.kt new file mode 100644 index 00000000..c7566f6d --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/KeyRingProtectionSettings.kt @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.protection + +import org.pgpainless.algorithm.HashAlgorithm +import org.pgpainless.algorithm.SymmetricKeyAlgorithm + +/** + * Secret key protection settings for iterated and salted S2K. The salt gets randomly chosen by the + * library each time. Note, that the s2kCount is the already encoded single-octet number. + * + * @param encryptionAlgorithm encryption algorithm + * @param hashAlgorithm hash algorithm + * @param s2kCount encoded (!) s2k iteration count + * @see Encoding Formula + */ +data class KeyRingProtectionSettings( + val encryptionAlgorithm: SymmetricKeyAlgorithm, + val hashAlgorithm: HashAlgorithm, + val s2kCount: Int +) { + + /** + * Create a [KeyRingProtectionSettings] object using the given encryption algorithm, + * [HashAlgorithm.SHA1] and 65536 iterations. It is okay to use SHA1 here, since we don't care + * about collisions. + * + * @param encryptionAlgorithm encryption algorithm + */ + constructor( + encryptionAlgorithm: SymmetricKeyAlgorithm + ) : this(encryptionAlgorithm, HashAlgorithm.SHA1, 0x60) + + init { + require(encryptionAlgorithm != SymmetricKeyAlgorithm.NULL) { + "Unencrypted is not allowed here!" + } + require(s2kCount > 0) { "s2kCount cannot be less than 1." } + } + + companion object { + + /** + * Secure default settings using [SymmetricKeyAlgorithm.AES_256], [HashAlgorithm.SHA256] and + * an iteration count of 65536. + * + * @return secure protection settings + */ + @JvmStatic + fun secureDefaultSettings() = + KeyRingProtectionSettings(SymmetricKeyAlgorithm.AES_256, HashAlgorithm.SHA256, 0x60) + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/PasswordBasedSecretKeyRingProtector.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/PasswordBasedSecretKeyRingProtector.kt new file mode 100644 index 00000000..9eb47e88 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/PasswordBasedSecretKeyRingProtector.kt @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.protection + +import org.bouncycastle.openpgp.PGPKeyRing +import org.bouncycastle.openpgp.PGPSecretKey +import org.pgpainless.key.protection.passphrase_provider.SecretKeyPassphraseProvider +import org.pgpainless.util.Passphrase + +/** + * Provides [PBESecretKeyDecryptor] and [PBESecretKeyEncryptor] objects while getting the + * passphrases from a [SecretKeyPassphraseProvider] and using settings from an + * [KeyRingProtectionSettings]. + */ +class PasswordBasedSecretKeyRingProtector : BaseSecretKeyRingProtector { + + constructor(passphraseProvider: SecretKeyPassphraseProvider) : super(passphraseProvider) + + /** + * Constructor. Passphrases for keys are sourced from the `passphraseProvider` and + * decryptors/encryptors are constructed following the settings given in `settings`. + * + * @param settings S2K settings etc. + * @param passphraseProvider provider which provides passphrases. + */ + constructor( + settings: KeyRingProtectionSettings, + passphraseProvider: SecretKeyPassphraseProvider + ) : super(passphraseProvider, settings) + + companion object { + @JvmStatic + fun forKey( + keyRing: PGPKeyRing, + passphrase: Passphrase + ): PasswordBasedSecretKeyRingProtector { + return object : SecretKeyPassphraseProvider { + + override fun getPassphraseFor(keyId: Long?): Passphrase? { + return if (hasPassphrase(keyId)) passphrase else null + } + + override fun hasPassphrase(keyId: Long?): Boolean { + return keyId != null && keyRing.getPublicKey(keyId) != null + } + } + .let { PasswordBasedSecretKeyRingProtector(it) } + } + + @JvmStatic + fun forKey(key: PGPSecretKey, passphrase: Passphrase): PasswordBasedSecretKeyRingProtector = + forKeyId(key.publicKey.keyID, passphrase) + + @JvmStatic + fun forKeyId( + singleKeyId: Long, + passphrase: Passphrase + ): PasswordBasedSecretKeyRingProtector { + return object : SecretKeyPassphraseProvider { + override fun getPassphraseFor(keyId: Long?): Passphrase? { + return if (hasPassphrase(keyId)) passphrase else null + } + + override fun hasPassphrase(keyId: Long?): Boolean { + return keyId == singleKeyId + } + } + .let { PasswordBasedSecretKeyRingProtector(it) } + } + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/SecretKeyRingProtector.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/SecretKeyRingProtector.kt new file mode 100644 index 00000000..5e86d950 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/SecretKeyRingProtector.kt @@ -0,0 +1,156 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.protection + +import kotlin.jvm.Throws +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPSecretKey +import org.bouncycastle.openpgp.PGPSecretKeyRing +import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor +import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor +import org.pgpainless.key.protection.passphrase_provider.SecretKeyPassphraseProvider +import org.pgpainless.key.protection.passphrase_provider.SolitaryPassphraseProvider +import org.pgpainless.util.Passphrase + +/** + * Task of the [SecretKeyRingProtector] is to map encryptor/decryptor objects to key-ids. + * [PBESecretKeyEncryptor]/[PBESecretKeyDecryptor] are used to encrypt/decrypt secret keys using a + * passphrase. + * + * While it is easy to create an implementation of this interface that fits your needs, there are a + * bunch of implementations ready for use. + */ +interface SecretKeyRingProtector { + + /** + * Returns true, if the protector has a passphrase for the key with the given key-id. + * + * @param keyId key id + * @return true if it has a passphrase, false otherwise + */ + fun hasPassphraseFor(keyId: Long): Boolean + + /** + * Return a decryptor for the key of id `keyId`. This method returns null if the key is + * unprotected. + * + * @param keyId id of the key + * @return decryptor for the key + */ + @Throws(PGPException::class) fun getDecryptor(keyId: Long): PBESecretKeyDecryptor? + + /** + * Return an encryptor for the key of id `keyId`. This method returns null if the key is + * unprotected. + * + * @param keyId id of the key + * @return encryptor for the key + */ + @Throws(PGPException::class) fun getEncryptor(keyId: Long): PBESecretKeyEncryptor? + + companion object { + + /** + * Return a protector for secret keys. The protector maintains an in-memory cache of + * passphrases and can be extended with new passphrases at runtime. + * + * See [CachingSecretKeyRingProtector] for how to memorize/forget additional passphrases + * during runtime. + * + * @param missingPassphraseCallback callback that is used to provide missing passphrases. + * @return caching secret key protector + */ + @JvmStatic + fun defaultSecretKeyRingProtector( + missingPassphraseCallback: SecretKeyPassphraseProvider? + ): CachingSecretKeyRingProtector = + CachingSecretKeyRingProtector( + mapOf(), + KeyRingProtectionSettings.secureDefaultSettings(), + missingPassphraseCallback) + + /** + * Use the provided passphrase to lock/unlock all keys in the provided key ring. + * + * This protector will use the provided passphrase to lock/unlock all subkeys present in the + * provided keys object. For other keys that are not present in the ring, it will return + * null. + * + * @param passphrase passphrase + * @param keys key ring + * @return protector + */ + @JvmStatic + fun unlockEachKeyWith( + passphrase: Passphrase, + keys: PGPSecretKeyRing + ): SecretKeyRingProtector = + fromPassphraseMap(keys.map { it.keyID }.associateWith { passphrase }) + + /** + * Use the provided passphrase to unlock any key. + * + * @param passphrase passphrase + * @return protector + */ + @JvmStatic + fun unlockAnyKeyWith(passphrase: Passphrase): SecretKeyRingProtector = + BaseSecretKeyRingProtector(SolitaryPassphraseProvider(passphrase)) + + /** + * Use the provided passphrase to lock/unlock only the provided (sub-)key. This protector + * will only return a non-null encryptor/decryptor based on the provided passphrase if + * [getEncryptor]/[getDecryptor] is getting called with the key-id of the provided key. + * + * Otherwise, this protector will always return null. + * + * @param passphrase passphrase + * @param key key to lock/unlock + * @return protector + */ + @JvmStatic + fun unlockSingleKeyWith(passphrase: Passphrase, key: PGPSecretKey): SecretKeyRingProtector = + PasswordBasedSecretKeyRingProtector.forKey(key, passphrase) + + /** + * Use the provided passphrase to lock/unlock only the provided (sub-)key. This protector + * will only return a non-null encryptor/decryptor based on the provided passphrase if + * [getEncryptor]/[getDecryptor] is getting called with the key-id of the provided key. + * + * Otherwise, this protector will always return null. + * + * @param passphrase passphrase + * @param keyId id of the key to lock/unlock + * @return protector + */ + @JvmStatic + fun unlockSingleKeyWith(passphrase: Passphrase, keyId: Long): SecretKeyRingProtector = + PasswordBasedSecretKeyRingProtector.forKeyId(keyId, passphrase) + + /** + * Protector for unprotected keys. This protector returns null for all + * [getEncryptor]/[getDecryptor] calls, no matter what the key-id is. + * + * As a consequence, this protector can only "unlock" keys which are not protected using a + * passphrase, and it will leave keys unprotected, should it be used to "protect" a key + * (e.g. in + * [org.pgpainless.key.modification.secretkeyring.SecretKeyRingEditor.changePassphraseFromOldPassphrase]). + * + * @return protector + */ + @JvmStatic fun unprotectedKeys() = UnprotectedKeysProtector() + + /** + * Use the provided map of key-ids and passphrases to unlock keys. + * + * @param passphraseMap map of key ids and their respective passphrases + * @return protector + */ + @JvmStatic + fun fromPassphraseMap(passphraseMap: Map): SecretKeyRingProtector = + CachingSecretKeyRingProtector( + passphraseMap, KeyRingProtectionSettings.secureDefaultSettings(), null) + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/UnlockSecretKey.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/UnlockSecretKey.kt new file mode 100644 index 00000000..b3b0308f --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/UnlockSecretKey.kt @@ -0,0 +1,78 @@ +// Copyright 2023 Paul Schaub. +// SPDX-FileCopyrightText: 2021 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.protection + +import kotlin.jvm.Throws +import openpgp.openPgpKeyId +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPPrivateKey +import org.bouncycastle.openpgp.PGPSecretKey +import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor +import org.pgpainless.PGPainless +import org.pgpainless.bouncycastle.extensions.isEncrypted +import org.pgpainless.exception.KeyIntegrityException +import org.pgpainless.exception.WrongPassphraseException +import org.pgpainless.key.util.PublicKeyParameterValidationUtil +import org.pgpainless.util.Passphrase + +class UnlockSecretKey { + + companion object { + + @JvmStatic + @Throws(PGPException::class, KeyIntegrityException::class) + fun unlockSecretKey( + secretKey: PGPSecretKey, + protector: SecretKeyRingProtector + ): PGPPrivateKey { + return if (secretKey.isEncrypted()) { + unlockSecretKey(secretKey, protector.getDecryptor(secretKey.keyID)) + } else { + unlockSecretKey(secretKey, null as PBESecretKeyDecryptor?) + } + } + + @JvmStatic + @Throws(PGPException::class) + fun unlockSecretKey( + secretKey: PGPSecretKey, + decryptor: PBESecretKeyDecryptor? + ): PGPPrivateKey { + val privateKey = + try { + secretKey.extractPrivateKey(decryptor) + } catch (e: PGPException) { + throw WrongPassphraseException(secretKey.keyID, e) + } + + if (privateKey == null) { + if (secretKey.s2K.type in 100..110) { + throw PGPException( + "Cannot decrypt secret key ${secretKey.keyID.openPgpKeyId()}: \n" + + "Unsupported private S2K type ${secretKey.s2K.type}") + } + throw PGPException("Cannot decrypt secret key.") + } + + if (PGPainless.getPolicy().isEnableKeyParameterValidation()) { + PublicKeyParameterValidationUtil.verifyPublicKeyParameterIntegrity( + privateKey, secretKey.publicKey) + } + + return privateKey + } + + @JvmStatic + fun unlockSecretKey(secretKey: PGPSecretKey, passphrase: Passphrase?): PGPPrivateKey { + return if (passphrase == null) { + unlockSecretKey(secretKey, SecretKeyRingProtector.unprotectedKeys()) + } else { + unlockSecretKey( + secretKey, SecretKeyRingProtector.unlockSingleKeyWith(passphrase, secretKey)) + } + } + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/UnprotectedKeysProtector.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/UnprotectedKeysProtector.kt new file mode 100644 index 00000000..a25bb31a --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/UnprotectedKeysProtector.kt @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 +package org.pgpainless.key.protection + +/** + * Implementation of the [SecretKeyRingProtector] which assumes that all handled keys are not + * password protected. + */ +class UnprotectedKeysProtector : SecretKeyRingProtector { + override fun hasPassphraseFor(keyId: Long) = true + + override fun getDecryptor(keyId: Long) = null + + override fun getEncryptor(keyId: Long) = null +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/fixes/S2KUsageFix.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/fixes/S2KUsageFix.kt new file mode 100644 index 00000000..a1a9f6c2 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/fixes/S2KUsageFix.kt @@ -0,0 +1,85 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.protection.fixes + +import org.bouncycastle.bcpg.SecretKeyPacket +import org.bouncycastle.openpgp.PGPSecretKey +import org.bouncycastle.openpgp.PGPSecretKeyRing +import org.pgpainless.algorithm.HashAlgorithm +import org.pgpainless.bouncycastle.extensions.unlock +import org.pgpainless.exception.WrongPassphraseException +import org.pgpainless.implementation.ImplementationFactory +import org.pgpainless.key.protection.SecretKeyRingProtector + +/** + * Repair class to fix keys which use S2K usage of value [SecretKeyPacket.USAGE_CHECKSUM]. The + * method [replaceUsageChecksumWithUsageSha1] ensures that such keys are encrypted using S2K usage + * [SecretKeyPacket.USAGE_SHA1] instead. + * + * @see Related PGPainless Bug + * Report + * @see Related PGPainless Feature + * Request + * @see Related upstream BC bug report + */ +class S2KUsageFix { + + companion object { + + /** + * Repair method for keys which use S2K usage
USAGE_CHECKSUM
which is deemed + * insecure. This method fixes the private keys by changing them to
USAGE_SHA1
+ * instead. + * + * @param keys keys + * @param protector protector to unlock and re-lock affected private keys + * @param skipKeysWithMissingPassphrase if set to true, missing subkey passphrases will + * cause the subkey to stay unaffected. + * @return fixed key ring + * @throws PGPException in case of a PGP error. + */ + @JvmStatic + @JvmOverloads + fun replaceUsageChecksumWithUsageSha1( + keys: PGPSecretKeyRing, + protector: SecretKeyRingProtector, + skipKeysWithMissingPassphrase: Boolean = false + ): PGPSecretKeyRing { + val digestCalculator = + ImplementationFactory.getInstance().getPGPDigestCalculator(HashAlgorithm.SHA1) + val keyList = mutableListOf() + for (key in keys) { + // CHECKSUM is not recommended + if (key.s2KUsage != SecretKeyPacket.USAGE_CHECKSUM) { + keyList.add(key) + continue + } + + val keyId = key.keyID + val encryptor = protector.getEncryptor(keyId) + if (encryptor == null) { + if (skipKeysWithMissingPassphrase) { + keyList.add(key) + continue + } + throw WrongPassphraseException( + "Missing passphrase for key with ID " + java.lang.Long.toHexString(keyId)) + } + + val privateKey = key.unlock(protector) + // This constructor makes use of USAGE_SHA1 by default + val fixedKey = + PGPSecretKey( + privateKey, + key.publicKey, + digestCalculator, + key.isMasterKey, + protector.getEncryptor(keyId)) + keyList.add(fixedKey) + } + return PGPSecretKeyRing(keyList) + } + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/passphrase_provider/MapBasedPassphraseProvider.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/passphrase_provider/MapBasedPassphraseProvider.kt new file mode 100644 index 00000000..3457cff7 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/passphrase_provider/MapBasedPassphraseProvider.kt @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.protection.passphrase_provider + +import org.pgpainless.util.Passphrase + +/** + * Implementation of the [SecretKeyPassphraseProvider] that holds a map of key-IDs and respective + * [Passphrase]. It will return the right passphrase depending on the key-id. + * + * Note: This provider might return null! + * + * TODO: Make this null-safe and throw an exception instead? + */ +class MapBasedPassphraseProvider(val map: Map) : SecretKeyPassphraseProvider { + + override fun getPassphraseFor(keyId: Long?): Passphrase? = map[keyId] + + override fun hasPassphrase(keyId: Long?): Boolean = map.containsKey(keyId) +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/passphrase_provider/SecretKeyPassphraseProvider.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/passphrase_provider/SecretKeyPassphraseProvider.kt new file mode 100644 index 00000000..a80b8bb0 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/passphrase_provider/SecretKeyPassphraseProvider.kt @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.protection.passphrase_provider + +import org.bouncycastle.openpgp.PGPSecretKey +import org.pgpainless.util.Passphrase + +/** Interface to allow the user to provide a [Passphrase] for an encrypted OpenPGP secret key. */ +interface SecretKeyPassphraseProvider { + + /** + * Return a passphrase for the given secret key. If no record is found, return null. Note: In + * case of an unprotected secret key, this method must may not return null, but a [Passphrase] + * with a content of null. + * + * @param secretKey secret key + * @return passphrase or null, if no passphrase record is found. + */ + fun getPassphraseFor(secretKey: PGPSecretKey): Passphrase? { + return getPassphraseFor(secretKey.keyID) + } + + /** + * Return a passphrase for the given key. If no record has been found, return null. Note: In + * case of an unprotected secret key, this method must may not return null, but a [Passphrase] + * with a content of null. + * + * @param keyId if of the secret key + * @return passphrase or null, if no passphrase record has been found. + */ + fun getPassphraseFor(keyId: Long?): Passphrase? + + fun hasPassphrase(keyId: Long?): Boolean +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/passphrase_provider/SolitaryPassphraseProvider.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/passphrase_provider/SolitaryPassphraseProvider.kt new file mode 100644 index 00000000..a9f6801d --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/passphrase_provider/SolitaryPassphraseProvider.kt @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.protection.passphrase_provider + +import org.pgpainless.util.Passphrase + +/** Implementation of the [SecretKeyPassphraseProvider] that holds a single [Passphrase]. */ +class SolitaryPassphraseProvider(val passphrase: Passphrase?) : SecretKeyPassphraseProvider { + + override fun getPassphraseFor(keyId: Long?): Passphrase? = passphrase + + override fun hasPassphrase(keyId: Long?): Boolean = true +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyIdUtil.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyIdUtil.kt new file mode 100644 index 00000000..3f1b98b1 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyIdUtil.kt @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.util + +import openpgp.fromOpenPgpKeyId +import openpgp.openPgpKeyId + +class KeyIdUtil { + + companion object { + + /** + * Convert a long key-id into a key-id. A long key-id is a 16 digit hex string. + * + * @param longKeyId 16-digit hexadecimal string + * @return key-id converted to [Long]. + */ + @JvmStatic + @Deprecated( + "Superseded by Long extension method.", ReplaceWith("Long.fromHexKeyId(longKeyId)")) + fun fromLongKeyId(longKeyId: String) = Long.fromOpenPgpKeyId(longKeyId) + + /** + * Format a long key-ID as upper-case hex string. + * + * @param keyId keyId + * @return hex encoded key ID + */ + @JvmStatic + @Deprecated("Superseded by Long extension method.", ReplaceWith("keyId.hexKeyId()")) + fun formatKeyId(keyId: Long) = keyId.openPgpKeyId() + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyRingUtils.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyRingUtils.kt new file mode 100644 index 00000000..f83b5486 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyRingUtils.kt @@ -0,0 +1,528 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.util + +import java.io.ByteArrayOutputStream +import kotlin.jvm.Throws +import openpgp.openPgpKeyId +import org.bouncycastle.bcpg.S2K +import org.bouncycastle.bcpg.SecretKeyPacket +import org.bouncycastle.openpgp.* +import org.bouncycastle.util.Strings +import org.pgpainless.bouncycastle.extensions.certificate +import org.pgpainless.bouncycastle.extensions.requireSecretKey +import org.pgpainless.exception.MissingPassphraseException +import org.pgpainless.implementation.ImplementationFactory +import org.pgpainless.key.protection.SecretKeyRingProtector +import org.pgpainless.key.protection.fixes.S2KUsageFix +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +class KeyRingUtils { + + companion object { + + @JvmStatic private val LOGGER: Logger = LoggerFactory.getLogger(KeyRingUtils::class.java) + + /** + * Return the primary [PGPSecretKey] from the provided [PGPSecretKeyRing]. If it has no + * primary secret key, throw a [NoSuchElementException]. + * + * @param secretKeys secret keys + * @return primary secret key + */ + @JvmStatic + @Deprecated( + "Deprecated in favor of PGPSecretKeyRing extension function.", + ReplaceWith("secretKeys.requireSecretKey(keyId)")) + fun requirePrimarySecretKeyFrom(secretKeys: PGPSecretKeyRing): PGPSecretKey { + return secretKeys.requireSecretKey(secretKeys.publicKey.keyID) + } + + /** + * Return the primary secret key from the given secret key ring. If the key ring only + * contains subkeys (e.g. if the primary secret key is stripped), this method may return + * null. + * + * @param secretKeys secret key ring + * @return primary secret key + */ + @JvmStatic + fun getPrimarySecretKeyFrom(secretKeys: PGPSecretKeyRing): PGPSecretKey? { + return secretKeys.secretKey.let { + if (it.isMasterKey) { + it + } else { + null + } + } + } + + /** + * Return the primary [PGPPublicKey] from the provided key ring. Throws a + * [NoSuchElementException] if the key ring has no primary public key. + * + * @param keyRing key ring + * @return primary public key + */ + @JvmStatic + fun requirePrimaryPublicKeyFrom(keyRing: PGPKeyRing): PGPPublicKey { + return getPrimaryPublicKey(keyRing) + ?: throw NoSuchElementException("Provided PGPKeyRing has no primary public key.") + } + + /** + * Return the primary [PGPPublicKey] from the provided key ring or null if it has none. + * + * @param keyRing key ring + * @return primary public key + */ + @JvmStatic + fun getPrimaryPublicKey(keyRing: PGPKeyRing): PGPPublicKey? { + return keyRing.publicKey.let { + if (it.isMasterKey) { + it + } else { + null + } + } + } + + /** + * Require the public key with the given subKeyId from the keyRing. If no such subkey + * exists, throw an [NoSuchElementException]. + * + * @param keyRing key ring + * @param subKeyId subkey id + * @return subkey + */ + @JvmStatic + fun requirePublicKeyFrom(keyRing: PGPKeyRing, subKeyId: Long): PGPPublicKey { + return keyRing.getPublicKey(subKeyId) + ?: throw NoSuchElementException( + "KeyRing does not contain public key with keyId ${subKeyId.openPgpKeyId()}.") + } + + /** + * Require the secret key with the given secret subKeyId from the secret keyRing. If no such + * subkey exists, throw an [NoSuchElementException]. + * + * @param keyRing secret key ring + * @param subKeyId subkey id + * @return secret subkey + */ + @JvmStatic + fun requireSecretKeyFrom(keyRing: PGPSecretKeyRing, subKeyId: Long): PGPSecretKey { + return keyRing.getSecretKey(subKeyId) + ?: throw NoSuchElementException( + "KeyRing does not contain secret key with keyID ${subKeyId.openPgpKeyId()}.") + } + + @JvmStatic + fun publicKeys(keys: PGPKeyRing): PGPPublicKeyRing { + return when (keys) { + is PGPPublicKeyRing -> keys + is PGPSecretKeyRing -> keys.certificate + else -> throw IllegalArgumentException("Unknown keys class: ${keys.javaClass.name}") + } + } + + /** + * Extract a [PGPPublicKeyRing] containing all public keys from the provided + * [PGPSecretKeyRing]. + * + * @param secretKeys secret key ring + * @return public key ring + */ + @JvmStatic + @Deprecated( + "Deprecated in favor of PGPSecretKeyRing extension method.", + ReplaceWith("secretKeys.certificate", "org.bouncycastle.extensions.certificate")) + fun publicKeyRingFrom(secretKeys: PGPSecretKeyRing): PGPPublicKeyRing { + return secretKeys.certificate + } + + /** + * Extract [PGPPublicKeyRings][PGPPublicKeyRing] from all + * [PGPSecretKeyRings][PGPSecretKeyRing] in the given [PGPSecretKeyRingCollection] and + * return them as a [PGPPublicKeyRingCollection]. + * + * @param secretKeyRings secret key ring collection + * @return public key ring collection + */ + @JvmStatic + fun publicKeyRingCollectionFrom( + secretKeyRings: PGPSecretKeyRingCollection + ): PGPPublicKeyRingCollection { + return PGPPublicKeyRingCollection( + secretKeyRings.keyRings.asSequence().map { it.certificate }.toList()) + } + + /** + * Create a new [PGPPublicKeyRingCollection] from an array of + * [PGPPublicKeyRings][PGPPublicKeyRing]. + * + * @param certificates array of public key rings + * @return key ring collection + */ + @JvmStatic + fun keyRingsToKeyRingCollection( + vararg certificates: PGPPublicKeyRing + ): PGPPublicKeyRingCollection { + return PGPPublicKeyRingCollection(certificates.toList()) + } + + /** + * Create a new [PGPSecretKeyRingCollection] from an array of + * [PGPSecretKeyRings][PGPSecretKeyRing]. + * + * @param secretKeys array of secret key rings + * @return secret key ring collection + */ + @JvmStatic + fun keyRingsToKeyRingCollection( + vararg secretKeys: PGPSecretKeyRing + ): PGPSecretKeyRingCollection { + return PGPSecretKeyRingCollection(secretKeys.toList()) + } + + /** + * Return true, if the given [PGPPublicKeyRing] contains a [PGPPublicKey] for the given key + * id. + * + * @param certificate public key ring + * @param keyId id of the key in question + * @return true if ring contains said key, false otherwise + */ + @JvmStatic + fun keyRingContainsKeyWithId(certificate: PGPPublicKeyRing, keyId: Long): Boolean { + return certificate.getPublicKey(keyId) != null + } + + /** + * Inject a key certification for the primary key into the given key ring. + * + * @param keyRing key ring + * @param certification key signature + * @param either [PGPPublicKeyRing] or [PGPSecretKeyRing] + * @return key ring with injected signature + */ + @JvmStatic + fun injectCertification(keyRing: T, certification: PGPSignature): T { + return injectCertification(keyRing, keyRing.publicKey, certification) + } + + /** + * Inject a key certification for the given key into the given key ring. + * + * @param keyRing key ring + * @param certifiedKey signed public key + * @param certification key signature + * @param either [PGPPublicKeyRing] or [PGPSecretKeyRing] + * @return key ring with injected signature + * @throws NoSuchElementException in case that the signed key is not part of the key ring + */ + @JvmStatic + fun injectCertification( + keyRing: T, + certifiedKey: PGPPublicKey, + certification: PGPSignature + ): T { + val secretAndPublicKeys = secretAndPublicKeys(keyRing) + val secretKeys: PGPSecretKeyRing? = secretAndPublicKeys.first + var certificate: PGPPublicKeyRing = secretAndPublicKeys.second + + if (!keyRingContainsKeyWithId(certificate, certifiedKey.keyID)) { + throw NoSuchElementException( + "Cannot find public key with id ${certifiedKey.keyID.openPgpKeyId()} in the provided key ring.") + } + + certificate = + PGPPublicKeyRing( + certificate.publicKeys + .asSequence() + .map { + if (it.keyID == certifiedKey.keyID) { + PGPPublicKey.addCertification(it, certification) + } else { + it + } + } + .toList()) + return if (secretKeys == null) { + certificate as T + } else { + PGPSecretKeyRing.replacePublicKeys(secretKeys, certificate) as T + } + } + + /** + * Inject a user-id certification into the given key ring. + * + * @param keyRing key ring + * @param userId signed user-id + * @param certification signature + * @param either [PGPPublicKeyRing] or [PGPSecretKeyRing] + * @return key ring with injected certification + */ + @JvmStatic + fun injectCertification( + keyRing: T, + userId: CharSequence, + certification: PGPSignature + ): T { + val secretAndPublicKeys = secretAndPublicKeys(keyRing) + val secretKeys: PGPSecretKeyRing? = secretAndPublicKeys.first + var certificate: PGPPublicKeyRing = secretAndPublicKeys.second + + certificate = + PGPPublicKeyRing( + listOf( + PGPPublicKey.addCertification( + requirePrimaryPublicKeyFrom(certificate), + userId.toString(), + certification)) + .plus(certificate.publicKeys.asSequence().drop(1))) + + return if (secretKeys == null) { + certificate as T + } else { + PGPSecretKeyRing.replacePublicKeys(secretKeys, certificate) as T + } + } + + /** + * Inject a user-attribute vector certification into the given key ring. + * + * @param keyRing key ring + * @param userAttributes certified user attributes + * @param certification certification signature + * @param either [PGPPublicKeyRing] or [PGPSecretKeyRing] + * @return key ring with injected user-attribute certification + */ + @JvmStatic + fun injectCertification( + keyRing: T, + userAttributes: PGPUserAttributeSubpacketVector, + certification: PGPSignature + ): T { + val secretAndPublicKeys = secretAndPublicKeys(keyRing) + val secretKeys: PGPSecretKeyRing? = secretAndPublicKeys.first + var certificate: PGPPublicKeyRing = secretAndPublicKeys.second + + certificate = + PGPPublicKeyRing( + listOf( + PGPPublicKey.addCertification( + requirePrimaryPublicKeyFrom(certificate), + userAttributes, + certification)) + .plus(certificate.publicKeys.asSequence().drop(1))) + + return if (secretKeys == null) { + certificate as T + } else { + PGPSecretKeyRing.replacePublicKeys(secretKeys, certificate) as T + } + } + + /** + * Inject a [PGPPublicKey] into the given key ring. + * + * @param keyRing key ring + * @param publicKey public key + * @param either [PGPPublicKeyRing] or [PGPSecretKeyRing] + * @return key ring with injected public key + */ + @JvmStatic + fun keysPlusPublicKey(keyRing: T, publicKey: PGPPublicKey): T { + val secretAndPublicKeys = secretAndPublicKeys(keyRing) + val secretKeys: PGPSecretKeyRing? = secretAndPublicKeys.first + var certificate: PGPPublicKeyRing = secretAndPublicKeys.second + + return if (secretKeys == null) { + PGPPublicKeyRing.insertPublicKey(certificate, publicKey) as T + } else { + PGPSecretKeyRing.insertOrReplacePublicKey(secretKeys, publicKey) as T + } + } + + @JvmStatic + private fun secretAndPublicKeys( + keyRing: PGPKeyRing + ): Pair { + var secretKeys: PGPSecretKeyRing? = null + val certificate: PGPPublicKeyRing + when (keyRing) { + is PGPSecretKeyRing -> { + secretKeys = keyRing + certificate = secretKeys.certificate + } + is PGPPublicKeyRing -> { + certificate = keyRing + } + else -> + throw IllegalArgumentException( + "keyRing is an unknown PGPKeyRing subclass: ${keyRing.javaClass.name}") + } + return secretKeys to certificate + } + + /** + * Inject a [PGPSecretKey] into a [PGPSecretKeyRing]. + * + * @param secretKeys secret key ring + * @param secretKey secret key + * @return secret key ring with injected secret key + */ + @JvmStatic + fun keysPlusSecretKey( + secretKeys: PGPSecretKeyRing, + secretKey: PGPSecretKey + ): PGPSecretKeyRing { + return PGPSecretKeyRing.insertSecretKey(secretKeys, secretKey) + } + + /** + * Inject the given signature into the public part of the given secret key. + * + * @param secretKey secret key + * @param signature signature + * @return secret key with the signature injected in its public key + */ + @JvmStatic + fun secretKeyPlusSignature(secretKey: PGPSecretKey, signature: PGPSignature): PGPSecretKey { + PGPPublicKey.addCertification(secretKey.publicKey, signature).let { + return PGPSecretKey.replacePublicKey(secretKey, it) + } + } + + /** + * Remove the secret key of the subkey identified by the given secret key id from the key + * ring. The public part stays attached to the key ring, so that it can still be used for + * encryption / verification of signatures. + * + * This method is intended to be used to remove secret primary keys from live keys when + * those are kept in offline storage. + * + * @param secretKeys secret key ring + * @param keyId id of the secret key to remove + * @return secret key ring with removed secret key + * @throws IOException in case of an error during serialization / deserialization of the key + * @throws PGPException in case of a broken key + */ + @JvmStatic + fun stripSecretKey(secretKeys: PGPSecretKeyRing, keyId: Long): PGPSecretKeyRing { + require(keyId != secretKeys.publicKey.keyID) { + "Bouncy Castle currently cannot deal with stripped primary secret keys." + } + if (secretKeys.getSecretKey(keyId) == null) { + throw NoSuchElementException( + "PGPSecretKeyRing does not contain secret key ${keyId.openPgpKeyId()}.") + } + + val out = ByteArrayOutputStream() + secretKeys.forEach { + if (it.keyID == keyId) { + // only encode the public key + it.publicKey.encode(out) + } else { + // else encode the whole secret + public key + it.encode(out) + } + } + secretKeys.extraPublicKeys.forEach { it.encode(out) } + return PGPSecretKeyRing( + out.toByteArray(), ImplementationFactory.getInstance().keyFingerprintCalculator) + } + + /** + * Strip all user-ids, user-attributes and signatures from the given public key. + * + * @param bloatedKey public key + * @return stripped public key + * @throws PGPException if the packet is faulty or the required calculations fail + */ + @JvmStatic + fun getStrippedDownPublicKey(bloatedKey: PGPPublicKey): PGPPublicKey { + return PGPPublicKey( + bloatedKey.publicKeyPacket, + ImplementationFactory.getInstance().keyFingerprintCalculator) + } + + @JvmStatic + fun getUserIdsIgnoringInvalidUTF8(key: PGPPublicKey): List { + return buildList { + key.rawUserIDs.forEach { + try { + add(Strings.fromUTF8ByteArray(it)) + } catch (e: IllegalArgumentException) { + LOGGER.warn("Invalid UTF-8 user-ID encountered: ${String(it)}") + } + } + } + } + + @JvmStatic + @Throws(MissingPassphraseException::class, PGPException::class) + fun changePassphrase( + keyId: Long?, + secretKeys: PGPSecretKeyRing, + oldProtector: SecretKeyRingProtector, + newProtector: SecretKeyRingProtector + ): PGPSecretKeyRing { + return if (keyId == null) { + PGPSecretKeyRing( + secretKeys.secretKeys + .asSequence() + .map { reencryptPrivateKey(it, oldProtector, newProtector) } + .toList()) + } else { + PGPSecretKeyRing( + secretKeys.secretKeys + .asSequence() + .map { + if (it.keyID == keyId) { + reencryptPrivateKey(it, oldProtector, newProtector) + } else { + it + } + } + .toList()) + } + .let { s2kUsageFixIfNecessary(it, newProtector) } + } + + @JvmStatic + fun reencryptPrivateKey( + secretKey: PGPSecretKey, + oldProtector: SecretKeyRingProtector, + newProtector: SecretKeyRingProtector + ): PGPSecretKey { + if (secretKey.s2K != null && secretKey.s2K.type == S2K.GNU_DUMMY_S2K) { + // If the key uses GNU_DUMMY_S2K we leave it as is + return secretKey + } + + return PGPSecretKey.copyWithNewPassword( + secretKey, + oldProtector.getDecryptor(secretKey.keyID), + newProtector.getEncryptor(secretKey.keyID)) + } + + @JvmStatic + fun s2kUsageFixIfNecessary( + secretKeys: PGPSecretKeyRing, + protector: SecretKeyRingProtector + ): PGPSecretKeyRing { + if (secretKeys.secretKeys.asSequence().any { + it.s2KUsage == SecretKeyPacket.USAGE_CHECKSUM + }) { + return S2KUsageFix.replaceUsageChecksumWithUsageSha1(secretKeys, protector, true) + } + return secretKeys + } + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/PublicKeyParameterValidationUtil.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/PublicKeyParameterValidationUtil.kt new file mode 100644 index 00000000..a1e79bf3 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/PublicKeyParameterValidationUtil.kt @@ -0,0 +1,286 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.util + +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.math.BigInteger +import java.security.SecureRandom +import org.bouncycastle.bcpg.* +import org.bouncycastle.openpgp.* +import org.bouncycastle.util.Arrays +import org.bouncycastle.util.io.Streams +import org.pgpainless.algorithm.HashAlgorithm +import org.pgpainless.algorithm.PublicKeyAlgorithm.Companion.requireFromId +import org.pgpainless.algorithm.SignatureType +import org.pgpainless.algorithm.SymmetricKeyAlgorithm +import org.pgpainless.bouncycastle.extensions.publicKeyAlgorithm +import org.pgpainless.exception.KeyIntegrityException +import org.pgpainless.implementation.ImplementationFactory.Companion.getInstance + +/** + * Utility class to verify keys against Key Overwriting (KO) attacks. This class of attacks is only + * possible if the attacker has access to the (encrypted) secret key material. To execute the + * attack, they would modify the unauthenticated parameters of the users public key. Using the + * modified public key in combination with the unmodified secret key material can then lead to the + * extraction of secret key parameters via weakly crafted messages. + * + * @see Key Overwriting (KO) Attacks against OpenPGP + */ +class PublicKeyParameterValidationUtil { + + companion object { + @JvmStatic + @Throws(KeyIntegrityException::class) + fun verifyPublicKeyParameterIntegrity(privateKey: PGPPrivateKey, publicKey: PGPPublicKey) { + val algorithm = publicKey.publicKeyAlgorithm + var valid = true + + val key = privateKey.privateKeyDataPacket + when (privateKey.privateKeyDataPacket) { + is RSASecretBCPGKey -> + valid = + verifyRSAKeyIntegrity( + key as RSASecretBCPGKey, + publicKey.publicKeyPacket.key as RSAPublicBCPGKey) + is EdSecretBCPGKey -> + valid = + verifyEdDsaKeyIntegrity( + key as EdSecretBCPGKey, + publicKey.publicKeyPacket.key as EdDSAPublicBCPGKey) + is DSASecretBCPGKey -> + valid = + verifyDsaKeyIntegrity( + key as DSASecretBCPGKey, + publicKey.publicKeyPacket.key as DSAPublicBCPGKey) + is ElGamalSecretBCPGKey -> + valid = + verifyElGamalKeyIntegrity( + key as ElGamalSecretBCPGKey, + publicKey.publicKeyPacket.key as ElGamalPublicBCPGKey) + } + + if (!valid) throw KeyIntegrityException() + + // Additional to the algorithm-specific tests further above, we also perform + // generic functionality tests with the key, such as whether it is able to decrypt + // encrypted data + // or verify signatures. + // These tests should be more or less constant time. + if (algorithm.isSigningCapable()) { + valid = verifyCanSign(privateKey, publicKey) + } + if (algorithm.isEncryptionCapable()) { + valid = valid and verifyCanDecrypt(privateKey, publicKey) + } + + if (!valid) throw KeyIntegrityException() + } + + @JvmStatic + @Throws(KeyIntegrityException::class) + private fun verifyRSAKeyIntegrity( + secretKey: RSASecretBCPGKey, + publicKey: RSAPublicBCPGKey + ): Boolean { + // Verify that the public keys N is equal to private keys p*q + return publicKey.modulus.equals(secretKey.primeP.multiply(secretKey.primeQ)) + } + + @JvmStatic + @Throws(KeyIntegrityException::class) + private fun verifyEdDsaKeyIntegrity( + secretKey: EdSecretBCPGKey, + publicKey: EdDSAPublicBCPGKey + ): Boolean { + // TODO: Implement + return true + } + + @JvmStatic + @Throws(KeyIntegrityException::class) + private fun verifyDsaKeyIntegrity( + privateKey: DSASecretBCPGKey, + publicKey: DSAPublicBCPGKey + ): Boolean { + // Not sure what value to put here in order to have a "robust" primality check + // I went with 40, since that's what SO recommends: + // https://stackoverflow.com/a/6330138 + val certainty = 40 + val pG = publicKey.g + val pP = publicKey.p + val pQ = publicKey.q + val pY = publicKey.y + val sX = privateKey.x + + val pPrime = pP.isProbablePrime(certainty) + if (!pPrime) { + return false + } + + val qPrime = pQ.isProbablePrime(certainty) + if (!qPrime) { + return false + } + + // q > 160 bits + val qLarge = pQ.bitLength() > 160 + if (!qLarge) { + return false + } + + // q divides p - 1 + val qDividesPminus1 = pP.subtract(BigInteger.ONE).mod(pQ) == BigInteger.ZERO + if (!qDividesPminus1) { + return false + } + + // 1 < g < p + val gInBounds = BigInteger.ONE.max(pG) == pG && pG.max(pP) == pP + if (!gInBounds) { + return false + } + + // g^q = 1 mod p + val gPowXModPEquals1 = pG.modPow(pQ, pP) == BigInteger.ONE + if (!gPowXModPEquals1) { + return false + } + + // y = g^x mod p + return pY == pG.modPow(sX, pP) + } + + /** + * Validate ElGamal public key parameters. + * + * Original implementation by the openpgpjs authors: false + else -> true + } + + /** + * Return true if the given reason code denotes a key revocation. + * + * @param code reason code + * @return is key revocation + */ + @JvmStatic + fun isKeyRevocation(code: Byte) = MAP[code]?.let { isKeyRevocation(it) } ?: false + + /** + * Return true if the given [Reason] denotes a key revocation. + * + * @param reason reason + * @return is key revocation + */ + @JvmStatic + fun isKeyRevocation(reason: Reason) = + when (reason) { + USER_ID_NO_LONGER_VALID -> false + else -> true + } + } + } + + enum class RevocationType { + KEY_REVOCATION, + CERT_REVOCATION + } + + companion object { + @JvmStatic fun createKeyRevocation() = WithReason(RevocationType.KEY_REVOCATION) + + @JvmStatic fun createCertificateRevocation() = WithReason(RevocationType.CERT_REVOCATION) + } + + class WithReason(val type: RevocationType) { + + fun withReason(reason: Reason): WithDescription { + require(reasonTypeMatches(reason, type)) { + "Reason $reason can only be used for ${if (type == RevocationType.KEY_REVOCATION) "certificate" else "key"} revocations." + } + return WithDescription(reason) + } + + private fun reasonTypeMatches(reason: Reason, type: RevocationType): Boolean { + return when (type) { + RevocationType.KEY_REVOCATION -> reason != Reason.USER_ID_NO_LONGER_VALID + RevocationType.CERT_REVOCATION -> + reason == Reason.USER_ID_NO_LONGER_VALID || reason == Reason.NO_REASON + } + } + } + + class WithDescription(val reason: Reason) { + fun withDescription(description: String): RevocationAttributes = + RevocationAttributes(reason, description) + + fun withoutDescription() = RevocationAttributes(reason, "") + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/UserId.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/UserId.kt new file mode 100644 index 00000000..c2b81700 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/UserId.kt @@ -0,0 +1,217 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.util + +class UserId internal constructor(name: String?, comment: String?, email: String?) : CharSequence { + + private val _name: String? + val comment: String? + val email: String? + + init { + this._name = name?.trim() + this.comment = comment?.trim() + this.email = email?.trim() + } + + val full: String = buildString { + if (name?.isNotBlank() == true) { + append(getName(true)) + } + if (comment?.isNotBlank() == true) { + if (isNotEmpty()) { + append(' ') + } + append("($comment)") + } + if (email?.isNotBlank() == true) { + if (isNotEmpty()) { + append(' ') + } + append("<$email>") + } + } + + override val length: Int + get() = full.length + + val name: String? + get() = getName(false) + + fun getName(preserveQuotes: Boolean): String? { + return if (preserveQuotes || _name.isNullOrBlank()) { + _name + } else _name.removeSurrounding("\"") + } + + override fun equals(other: Any?): Boolean { + if (other === null) { + return false + } + if (this === other) { + return true + } + if (other !is UserId) { + return false + } + return isComponentEqual(_name, other._name, false) && + isComponentEqual(comment, other.comment, false) && + isComponentEqual(email, other.email, true) + } + + override fun get(index: Int): Char { + return full[index] + } + + override fun hashCode(): Int { + return toString().hashCode() + } + + override fun subSequence(startIndex: Int, endIndex: Int): CharSequence { + return full.subSequence(startIndex, endIndex) + } + + override fun toString(): String { + return full + } + + private fun isComponentEqual( + value: String?, + otherValue: String?, + ignoreCase: Boolean + ): Boolean = value.equals(otherValue, ignoreCase) + + fun toBuilder() = + builder().also { builder -> + if (this._name != null) builder.withName(_name) + if (this.comment != null) builder.withComment(comment) + if (this.email != null) builder.withEmail(email) + } + + companion object { + + // Email regex: https://emailregex.com/ + // switched "a-z0-9" to "\p{L}\u0900-\u097F0-9" for better support for international + // characters + // \\p{L} = Unicode Letters + // \u0900-\u097F = Hindi Letters + @JvmStatic + private val emailPattern = + ("(?:[\\p{L}\\u0900-\\u097F0-9!#\\$%&'*+/=?^_`{|}~-]+(?:\\.[\\p{L}\\u0900-\\u097F0-9!#\\$%&'*+/=?^_`{|}~-]+)*|\"(?:[\\x01-\\x08\\x0b\\x0c\\x0e-" + + "\\x1f\\x21\\x23-\\x5b\\x5d-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])*\")@(?:(?:[\\p{L}\\u0900-\\u097F0-9](?:[\\p{L}\\u0900-\\u097F0-9" + + "-]*[\\p{L}\\u0900-\\u097F0-9])?\\.)+[\\p{L}\\u0900-\\u097F0-9](?:[\\p{L}\\u0900-\\u097F0-9-]*[\\p{L}\\u0900-\\u097F0-9])?|\\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)" + + "\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[$\\p{L}\\u0900-\\u097F0-9-]*[\\p{L}\\u0900-\\u097F0-9]:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f" + + "\\x21-\\x5a\\x53-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])+)])") + .toPattern() + + // User-ID Regex + // "Firstname Lastname (Comment) " + // All groups are optional + // https://www.rfc-editor.org/rfc/rfc5322#page-16 + @JvmStatic + private val nameAddrPattern = + "^((?.+?)\\s)?(\\((?.+?)\\)\\s)?(<(?.+?)>)?$".toPattern() + + /** + * Parse a [UserId] from free-form text,
name-addr
or
mailbox
string + * and split it up into its components. Example inputs for this method: + *
    + *
  • john@pgpainless.org
  • + *
  • <john@pgpainless.org>
  • + *
  • John Doe
  • + *
  • John Doe <john@pgpainless.org>
  • + *
  • John Doe (work email) <john@pgpainless.org>
  • + *
+ * + * In these cases, this method will detect email addresses, names and comments and expose + * those via the respective getters. This method does not support parsing mail addresses of + * the following formats: + *
    + *
  • Local domains without TLDs (
    user@localdomain1
    )
  • + *
  • " "@example.org
    (spaces between the quotes)
  • + *
  • "very.(),:;<>[]\".VERY.\"very@\\ \"very\".unusual"@strange.example.com
  • + *
+ * + * Note: This method does not guarantee that + *
string.equals(UserId.parse(string).toString())
is true. For example, + *
UserId.parse("alice@pgpainless.org").toString()
wraps the mail address in + * + * angled brackets. + * + * @param string user-id + * @return parsed UserId object + * @see
RFC5322 §3.4. Address + * Specification + */ + @JvmStatic + fun parse(string: String): UserId { + val trimmed = string.trim() + nameAddrPattern.matcher(trimmed).let { nameAddrMatcher -> + if (nameAddrMatcher.find()) { + val name = nameAddrMatcher.group(2) + val comment = nameAddrMatcher.group(4) + val mail = nameAddrMatcher.group(6) + require(emailPattern.matcher(mail).matches()) { "Malformed email address" } + return UserId(name, comment, mail) + } else { + require(emailPattern.matcher(trimmed).matches()) { "Malformed email address" } + return UserId(null, null, trimmed) + } + } + } + + @JvmStatic fun onlyEmail(email: String) = UserId(null, null, email) + + @JvmStatic fun nameAndEmail(name: String, email: String) = UserId(name, null, email) + + @JvmStatic + fun compare(u1: UserId?, u2: UserId?, comparator: Comparator) = + comparator.compare(u1, u2) + + @JvmStatic + @Deprecated("Deprecated in favor of builde() method.", ReplaceWith("builder()")) + fun newBuilder() = builder() + + @JvmStatic fun builder() = Builder() + } + + class Builder internal constructor() { + var name: String? = null + var comment: String? = null + var email: String? = null + + fun withName(name: String) = apply { this.name = name } + + fun withComment(comment: String) = apply { this.comment = comment } + + fun withEmail(email: String) = apply { this.email = email } + + fun noName() = apply { this.name = null } + + fun noComment() = apply { this.comment = null } + + fun noEmail() = apply { this.email = null } + + fun build() = UserId(name, comment, email) + } + + class DefaultComparator : Comparator { + override fun compare(o1: UserId?, o2: UserId?): Int { + return compareBy { it?._name } + .thenBy { it?.comment } + .thenBy { it?.email } + .compare(o1, o2) + } + } + + class DefaultIgnoreCaseComparator : Comparator { + override fun compare(p0: UserId?, p1: UserId?): Int { + return compareBy { it?._name?.lowercase() } + .thenBy { it?.comment?.lowercase() } + .thenBy { it?.email?.lowercase() } + .compare(p0, p1) + } + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/policy/Policy.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/policy/Policy.kt new file mode 100644 index 00000000..7c6bb2d3 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/policy/Policy.kt @@ -0,0 +1,410 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.policy + +import java.util.* +import org.pgpainless.algorithm.* +import org.pgpainless.util.DateUtil +import org.pgpainless.util.NotationRegistry + +class Policy( + var certificationSignatureHashAlgorithmPolicy: HashAlgorithmPolicy, + var revocationSignatureHashAlgorithmPolicy: HashAlgorithmPolicy, + var dataSignatureHashAlgorithmPolicy: HashAlgorithmPolicy, + var symmetricKeyEncryptionAlgorithmPolicy: SymmetricKeyAlgorithmPolicy, + var symmetricKeyDecryptionAlgorithmPolicy: SymmetricKeyAlgorithmPolicy, + var compressionAlgorithmPolicy: CompressionAlgorithmPolicy, + var publicKeyAlgorithmPolicy: PublicKeyAlgorithmPolicy, + var notationRegistry: NotationRegistry +) { + + constructor() : + this( + HashAlgorithmPolicy.smartCertificationSignatureHashAlgorithmPolicy(), + HashAlgorithmPolicy.smartCertificationSignatureHashAlgorithmPolicy(), + HashAlgorithmPolicy.smartDataSignatureHashAlgorithmPolicy(), + SymmetricKeyAlgorithmPolicy.symmetricKeyEncryptionPolicy2022(), + SymmetricKeyAlgorithmPolicy.symmetricKeyDecryptionPolicy2022(), + CompressionAlgorithmPolicy.anyCompressionAlgorithmPolicy(), + PublicKeyAlgorithmPolicy.bsi2021PublicKeyAlgorithmPolicy(), + NotationRegistry()) + + var keyGenerationAlgorithmSuite = AlgorithmSuite.defaultAlgorithmSuite + var signerUserIdValidationLevel = SignerUserIdValidationLevel.DISABLED + var enableKeyParameterValidation = false + + fun isEnableKeyParameterValidation() = enableKeyParameterValidation + + /** + * Create a HashAlgorithmPolicy which accepts all [HashAlgorithms][HashAlgorithm] from the given + * map, if the queried usage date is BEFORE the respective termination date. A termination date + * value of
null
means no termination, resulting in the algorithm being acceptable, + * regardless of usage date. + * + * @param defaultHashAlgorithm default hash algorithm + * @param algorithmTerminationDates map of acceptable algorithms and their termination dates + */ + class HashAlgorithmPolicy( + val defaultHashAlgorithm: HashAlgorithm, + val acceptableHashAlgorithmsAndTerminationDates: Map + ) { + + /** + * Create a [HashAlgorithmPolicy] which accepts all [HashAlgorithms][HashAlgorithm] listed + * in the given list, regardless of usage date. + * + * @param defaultHashAlgorithm default hash algorithm (e.g. used as fallback if negotiation + * fails) + * @param acceptableHashAlgorithms list of acceptable hash algorithms + */ + constructor( + defaultHashAlgorithm: HashAlgorithm, + acceptableHashAlgorithms: List + ) : this(defaultHashAlgorithm, acceptableHashAlgorithms.associateWith { null }) + + fun isAcceptable(hashAlgorithm: HashAlgorithm) = isAcceptable(hashAlgorithm, Date()) + + /** + * Return true, if the given algorithm is acceptable for the given usage date. + * + * @param hashAlgorithm algorithm + * @param referenceTime usage date (e.g. signature creation time) + * @return acceptance + */ + fun isAcceptable(hashAlgorithm: HashAlgorithm, referenceTime: Date): Boolean { + if (!acceptableHashAlgorithmsAndTerminationDates.containsKey(hashAlgorithm)) + return false + val terminationDate = + acceptableHashAlgorithmsAndTerminationDates[hashAlgorithm] ?: return true + return terminationDate > referenceTime + } + + fun isAcceptable(algorithmId: Int) = isAcceptable(algorithmId, Date()) + + fun isAcceptable(algorithmId: Int, referenceTime: Date): Boolean { + val algorithm = HashAlgorithm.fromId(algorithmId) ?: return false + return isAcceptable(algorithm, referenceTime) + } + + fun defaultHashAlgorithm() = defaultHashAlgorithm + + companion object { + // https://sequoia-pgp.org/blog/2023/02/01/202302-happy-sha1-day/ + // signature data which is not attacker-controlled is acceptable before 2023-02-01 + @JvmStatic + fun smartCertificationSignatureHashAlgorithmPolicy() = + HashAlgorithmPolicy( + HashAlgorithm.SHA512, + buildMap { + put(HashAlgorithm.SHA3_512, null) + put(HashAlgorithm.SHA3_512, null) + put(HashAlgorithm.SHA3_256, null) + put(HashAlgorithm.SHA512, null) + put(HashAlgorithm.SHA384, null) + put(HashAlgorithm.SHA256, null) + put(HashAlgorithm.SHA224, null) + put( + HashAlgorithm.RIPEMD160, + DateUtil.parseUTCDate("2023-02-01 00:00:00 UTC")) + put(HashAlgorithm.SHA1, DateUtil.parseUTCDate("2023-02-01 00:00:00 UTC")) + put(HashAlgorithm.MD5, DateUtil.parseUTCDate("1997-02-01 00:00:00 UTC")) + }) + + @JvmStatic + fun smartDataSignatureHashAlgorithmPolicy() = smartSignatureHashAlgorithmPolicy() + + @JvmStatic + fun smartSignatureHashAlgorithmPolicy() = + HashAlgorithmPolicy( + HashAlgorithm.SHA512, + buildMap { + put(HashAlgorithm.SHA3_512, null) + put(HashAlgorithm.SHA3_256, null) + put(HashAlgorithm.SHA512, null) + put(HashAlgorithm.SHA384, null) + put(HashAlgorithm.SHA256, null) + put(HashAlgorithm.SHA224, null) + put( + HashAlgorithm.RIPEMD160, + DateUtil.parseUTCDate("2013-02-01 00:00:00 UTC")) + put(HashAlgorithm.SHA1, DateUtil.parseUTCDate("2013-02-01 00:00:00 UTC")) + put(HashAlgorithm.MD5, DateUtil.parseUTCDate("1997-02-01 00:00:00 UTC")) + }) + + /** + * [HashAlgorithmPolicy] which only accepts signatures made using algorithms which are + * acceptable according to 2022 standards. + * + * Particularly this policy only accepts algorithms from the SHA2 and SHA3 families. + * + * @return static signature algorithm policy + */ + @JvmStatic + fun static2022SignatureHashAlgorithmPolicy() = + HashAlgorithmPolicy( + HashAlgorithm.SHA512, + listOf( + HashAlgorithm.SHA3_512, + HashAlgorithm.SHA3_256, + HashAlgorithm.SHA512, + HashAlgorithm.SHA384, + HashAlgorithm.SHA256, + HashAlgorithm.SHA224)) + + /** + * Hash algorithm policy for revocation signatures, which accepts SHA1 and SHA2 + * algorithms, as well as RIPEMD160. + * + * @return static revocation signature hash algorithm policy + */ + @JvmStatic + fun static2022RevocationSignatureHashAlgorithmPolicy() = + HashAlgorithmPolicy( + HashAlgorithm.SHA512, + listOf( + HashAlgorithm.SHA3_512, + HashAlgorithm.SHA3_256, + HashAlgorithm.SHA512, + HashAlgorithm.SHA384, + HashAlgorithm.SHA256, + HashAlgorithm.SHA224, + HashAlgorithm.SHA1, + HashAlgorithm.RIPEMD160)) + } + } + + class SymmetricKeyAlgorithmPolicy( + val defaultSymmetricKeyAlgorithm: SymmetricKeyAlgorithm, + val acceptableSymmetricKeyAlgorithms: List + ) { + + fun isAcceptable(algorithm: SymmetricKeyAlgorithm) = + acceptableSymmetricKeyAlgorithms.contains(algorithm) + + fun isAcceptable(algorithmId: Int): Boolean { + val algorithm = SymmetricKeyAlgorithm.fromId(algorithmId) ?: return false + return isAcceptable(algorithm) + } + + fun selectBest(options: List): SymmetricKeyAlgorithm? { + for (acceptable in acceptableSymmetricKeyAlgorithms) { + if (options.contains(acceptable)) { + return acceptable + } + } + return null + } + + companion object { + + /** + * The default symmetric encryption algorithm policy of PGPainless. + * + * @return default symmetric encryption algorithm policy + * @deprecated not expressive - will be removed in a future release + */ + @JvmStatic + @Deprecated( + "Not expressive - will be removed in a future release", + ReplaceWith("symmetricKeyEncryptionPolicy2022")) + fun defaultSymmetricKeyEncryptionAlgorithmPolicy() = symmetricKeyEncryptionPolicy2022() + + /** + * Policy for symmetric encryption algorithms in the context of message production + * (encryption). This suite contains algorithms that are deemed safe to use in 2022. + * + * @return 2022 symmetric key encryption algorithm policy + */ + @JvmStatic + fun symmetricKeyEncryptionPolicy2022() = + SymmetricKeyAlgorithmPolicy( + SymmetricKeyAlgorithm.AES_128, + // Reject: Unencrypted, IDEA, TripleDES, CAST5, Blowfish + listOf( + SymmetricKeyAlgorithm.AES_256, + SymmetricKeyAlgorithm.AES_192, + SymmetricKeyAlgorithm.AES_128, + SymmetricKeyAlgorithm.TWOFISH, + SymmetricKeyAlgorithm.CAMELLIA_256, + SymmetricKeyAlgorithm.CAMELLIA_192, + SymmetricKeyAlgorithm.CAMELLIA_128)) + + /** + * The default symmetric decryption algorithm policy of PGPainless. + * + * @return default symmetric decryption algorithm policy + * @deprecated not expressive - will be removed in a future update + */ + @JvmStatic + @Deprecated( + "not expressive - will be removed in a future update", + ReplaceWith("symmetricKeyDecryptionPolicy2022()")) + fun defaultSymmetricKeyDecryptionAlgorithmPolicy() = symmetricKeyDecryptionPolicy2022() + + /** + * Policy for symmetric key encryption algorithms in the context of message consumption + * (decryption). This suite contains algorithms that are deemed safe to use in 2022. + * + * @return 2022 symmetric key decryption algorithm policy + */ + @JvmStatic + fun symmetricKeyDecryptionPolicy2022() = + SymmetricKeyAlgorithmPolicy( + SymmetricKeyAlgorithm.AES_128, + // Reject: Unencrypted, IDEA, TripleDES, Blowfish + listOf( + SymmetricKeyAlgorithm.AES_256, + SymmetricKeyAlgorithm.AES_192, + SymmetricKeyAlgorithm.AES_128, + SymmetricKeyAlgorithm.TWOFISH, + SymmetricKeyAlgorithm.CAMELLIA_256, + SymmetricKeyAlgorithm.CAMELLIA_192, + SymmetricKeyAlgorithm.CAMELLIA_128, + SymmetricKeyAlgorithm.CAST5)) + } + } + + class CompressionAlgorithmPolicy( + val defaultCompressionAlgorithm: CompressionAlgorithm, + val acceptableCompressionAlgorithms: List + ) { + + fun isAcceptable(algorithm: CompressionAlgorithm) = + acceptableCompressionAlgorithms.contains(algorithm) + + fun isAcceptable(algorithmId: Int): Boolean { + val algorithm = CompressionAlgorithm.fromId(algorithmId) ?: return false + return isAcceptable(algorithm) + } + + fun defaultCompressionAlgorithm() = defaultCompressionAlgorithm + + companion object { + + /** + * Default [CompressionAlgorithmPolicy] of PGPainless. The default compression algorithm + * policy accepts any compression algorithm. + * + * @return default algorithm policy + * @deprecated not expressive - might be removed in a future release + */ + @JvmStatic + @Deprecated( + "not expressive - might be removed in a future release", + ReplaceWith("anyCompressionAlgorithmPolicy()")) + fun defaultCompressionAlgorithmPolicy() = anyCompressionAlgorithmPolicy() + + /** + * Policy that accepts any known compression algorithm and offers + * [CompressionAlgorithm.ZIP] as default algorithm. + * + * @return compression algorithm policy + */ + @JvmStatic + fun anyCompressionAlgorithmPolicy() = + CompressionAlgorithmPolicy( + CompressionAlgorithm.ZIP, + listOf( + CompressionAlgorithm.UNCOMPRESSED, + CompressionAlgorithm.ZIP, + CompressionAlgorithm.BZIP2, + CompressionAlgorithm.ZLIB)) + } + } + + class PublicKeyAlgorithmPolicy(private val algorithmStrengths: Map) { + + fun isAcceptable(algorithm: PublicKeyAlgorithm, bitStrength: Int): Boolean { + return bitStrength >= (algorithmStrengths[algorithm] ?: return false) + } + + fun isAcceptable(algorithmId: Int, bitStrength: Int): Boolean { + val algorithm = PublicKeyAlgorithm.fromId(algorithmId) ?: return false + return isAcceptable(algorithm, bitStrength) + } + + companion object { + + /** + * Return PGPainless' default public key algorithm policy. This policy is based upon + * recommendations made by the German Federal Office for Information Security (BSI). + * + * @return default algorithm policy + * @deprecated not expressive - might be removed in a future release + */ + @JvmStatic + @Deprecated( + "not expressive - might be removed in a future release", + ReplaceWith("bsi2021PublicKeyAlgorithmPolicy()")) + fun defaultPublicKeyAlgorithmPolicy() = bsi2021PublicKeyAlgorithmPolicy() + + /** + * This policy is based upon recommendations made by the German Federal Office for + * Information Security (BSI). + * + * Basically this policy requires keys based on elliptic curves to have a bit strength + * of at least 250, and keys based on prime number factorization / discrete logarithm + * problems to have a strength of at least 2000 bits. + * + * @return default algorithm policy + * @see BSI - + * Technical Guideline - Cryptographic Mechanisms: Recommendations and Key Lengths + * (2021-01) + * @see BlueKrypt | Cryptographic Key Length + * Recommendation + */ + @JvmStatic + fun bsi2021PublicKeyAlgorithmPolicy() = + PublicKeyAlgorithmPolicy( + buildMap { + // §5.4.1 + put(PublicKeyAlgorithm.RSA_GENERAL, 2000) + put(PublicKeyAlgorithm.RSA_SIGN, 2000) + put(PublicKeyAlgorithm.RSA_ENCRYPT, 2000) + // Note: ElGamal is not mentioned in the BSI document. + // We assume that the requirements are similar to other DH algorithms + put(PublicKeyAlgorithm.ELGAMAL_ENCRYPT, 2000) + put(PublicKeyAlgorithm.ELGAMAL_GENERAL, 2000) + // §5.4.2 + put(PublicKeyAlgorithm.DSA, 2000) + // §5.4.3 + put(PublicKeyAlgorithm.ECDSA, 250) + // Note: EdDSA is not mentioned in the BSI document. + // We assume that the requirements are similar to other EC algorithms. + put(PublicKeyAlgorithm.EDDSA_LEGACY, 250) + // §7.2.1 + put(PublicKeyAlgorithm.DIFFIE_HELLMAN, 2000) + // §7.2.2 + put(PublicKeyAlgorithm.ECDH, 250) + }) + } + } + + enum class SignerUserIdValidationLevel { + /** + * PGPainless will verify [org.bouncycastle.bcpg.sig.SignerUserID] subpackets in signatures + * strictly. This means, that signatures with Signer's User-ID subpackets containing a value + * that does not match the signer key's user-id exactly, will be rejected. E.g. Signer's + * user-id "alice@pgpainless.org", User-ID: "Alice <alice@pgpainless.org>" does not + * match exactly and is therefore rejected. + */ + STRICT, + + /** + * PGPainless will ignore [org.bouncycastle.bcpg.sig.SignerUserID] subpackets on signature. + */ + DISABLED + } + + companion object { + + @Volatile private var INSTANCE: Policy? = null + + @JvmStatic + fun getInstance() = + INSTANCE ?: synchronized(this) { INSTANCE ?: Policy().also { INSTANCE = it } } + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/provider/BouncyCastleProviderFactory.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/provider/BouncyCastleProviderFactory.kt new file mode 100644 index 00000000..27192953 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/provider/BouncyCastleProviderFactory.kt @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.provider + +import java.security.Provider +import org.bouncycastle.jce.provider.BouncyCastleProvider + +class BouncyCastleProviderFactory : ProviderFactory() { + override val securityProvider: Provider = BouncyCastleProvider() +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/provider/ProviderFactory.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/provider/ProviderFactory.kt new file mode 100644 index 00000000..531ae54b --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/provider/ProviderFactory.kt @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.provider + +import java.security.Provider + +/** + * Allow the use of different [Provider] implementations to provide cryptographic primitives by + * setting a [ProviderFactory] singleton. By default, the class is initialized with a + * [BouncyCastleProviderFactory]. To make use of your own custom [Provider], call [setFactory], + * passing your own custom [ProviderFactory] instance. + */ +abstract class ProviderFactory { + + protected abstract val securityProvider: Provider + protected open val securityProviderName: String + get() = securityProvider.name + + companion object { + // singleton instance + @JvmStatic var factory: ProviderFactory = BouncyCastleProviderFactory() + + @JvmStatic + val provider: Provider + @JvmName("getProvider") get() = factory.securityProvider + + @JvmStatic + val providerName: String + get() = factory.securityProviderName + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/SignatureUtils.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/SignatureUtils.kt new file mode 100644 index 00000000..770dfc56 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/SignatureUtils.kt @@ -0,0 +1,274 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature + +import java.io.IOException +import java.io.InputStream +import java.util.* +import openpgp.plusSeconds +import org.bouncycastle.bcpg.sig.KeyExpirationTime +import org.bouncycastle.openpgp.* +import org.bouncycastle.util.encoders.Hex +import org.bouncycastle.util.io.Streams +import org.pgpainless.bouncycastle.extensions.* +import org.pgpainless.implementation.ImplementationFactory +import org.pgpainless.key.OpenPgpFingerprint +import org.pgpainless.key.util.RevocationAttributes.Reason +import org.pgpainless.util.ArmorUtils + +const val MAX_ITERATIONS = 10000 + +class SignatureUtils { + companion object { + + /** + * Extract and return the key expiration date value from the given signature. If the + * signature does not carry a [KeyExpirationTime] subpacket, return null. + * + * @param keyCreationDate creation date of the key + * @param signature signature + * @return key expiration date as given by the signature + */ + @JvmStatic + @Deprecated( + "Deprecated in favor of PGPSignature extension method.", + ReplaceWith( + "signature.getKeyExpirationDate(keyCreationDate)", + "org.bouncycastle.extensions.getKeyExpirationDate")) + fun getKeyExpirationDate(keyCreationDate: Date, signature: PGPSignature): Date? { + return signature.getKeyExpirationDate(keyCreationDate) + } + + /** + * Return the expiration date of the signature. If the signature has no expiration date, + * this will return null. + * + * @param signature signature + * @return expiration date of the signature, or null if it does not expire. + */ + @JvmStatic + @Deprecated( + "Deprecated in favor of PGPSignature extension method.", + ReplaceWith( + "signature.signatureExpirationDate", + "org.bouncycastle.extensions.signatureExpirationDate")) + fun getSignatureExpirationDate(signature: PGPSignature): Date? = + signature.signatureExpirationDate + + /** + * Return a new date which represents the given date plus the given amount of seconds added. + * + * Since '0' is a special date value in the OpenPGP specification (e.g. '0' means no + * expiration for expiration dates), this method will return 'null' if seconds is 0. + * + * @param date date + * @param seconds number of seconds to be added + * @return date plus seconds or null if seconds is '0' + */ + @JvmStatic + @Deprecated( + "Deprecated in favor of Date extension method.", + ReplaceWith("date.plusSeconds(seconds)", "openpgp.plusSeconds")) + fun datePlusSeconds(date: Date, seconds: Long): Date? { + return date.plusSeconds(seconds) + } + + /** + * Return true, if the expiration date of the [PGPSignature] lays in the past. If no + * expiration date is present in the signature, it is considered non-expired. + * + * @param signature signature + * @return true if expired, false otherwise + */ + @JvmStatic + @Deprecated( + "Deprecated in favor of PGPSignature extension method.", + ReplaceWith("signature.isExpired()", "org.bouncycastle.extensions.isExpired")) + fun isSignatureExpired(signature: PGPSignature): Boolean { + return signature.isExpired() + } + + /** + * Return true, if the expiration date of the given [PGPSignature] is past the given + * comparison [Date]. If no expiration date is present in the signature, it is considered + * non-expiring. + * + * @param signature signature + * @param referenceTime reference date + * @return true if sig is expired at reference date, false otherwise + */ + @JvmStatic + @Deprecated( + "Deprecated in favor of PGPSignature extension method.", + ReplaceWith( + "signature.isExpired(referenceTime)", "org.bouncycastle.extensions.isExpired")) + fun isSignatureExpired(signature: PGPSignature, referenceTime: Date): Boolean { + return signature.isExpired(referenceTime) + } + + /** + * Return true if the provided signature is a hard revocation. Hard revocations are + * revocation signatures which either carry a revocation reason of [Reason.KEY_COMPROMISED] + * or [Reason.NO_REASON], or no reason at all. + * + * @param signature signature + * @return true if signature is a hard revocation + */ + @JvmStatic + @Deprecated( + "Deprecated in favor of PGPSignature extension function.", + ReplaceWith( + "signature.isHardRevocation", "org.bouncycastle.extensions.isHardRevocation")) + fun isHardRevocation(signature: PGPSignature): Boolean { + return signature.isHardRevocation + } + + @JvmStatic + fun readSignatures(encodedSignatures: String): List { + return readSignatures(encodedSignatures.toByteArray()) + } + + @JvmStatic + fun readSignatures(encodedSignatures: ByteArray): List { + return readSignatures(encodedSignatures.inputStream()) + } + + @JvmStatic + @Throws(IOException::class, PGPException::class) + fun readSignatures(inputStream: InputStream): List { + return readSignatures(inputStream, MAX_ITERATIONS) + } + + /** + * Read and return [PGPSignatures][PGPSignature]. This method can deal with signatures that + * may be binary, armored and may contain marker packets. + * + * @param inputStream input stream + * @param maxIterations number of loop iterations until reading is aborted + * @return list of encountered signatures + */ + @JvmStatic + fun readSignatures(inputStream: InputStream, maxIterations: Int): List { + val signatures = mutableListOf() + val pgpIn = ArmorUtils.getDecoderStream(inputStream) + val objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(pgpIn) + + var i = 0 + var nextObject: Any? = null + while (i++ < maxIterations && + objectFactory.nextObject().also { nextObject = it } != null) { + // Since signatures are indistinguishable from randomness, there is no point in + // having them compressed, + // except for an attacker who is trying to exploit flaws in the decompression + // algorithm. + // Therefore, we ignore compressed data packets without attempting decompression. + if (nextObject is PGPCompressedData) { + // getInputStream() does not do decompression, contrary to getDataStream(). + Streams.drain( + (nextObject as PGPCompressedData) + .inputStream) // Skip packet without decompressing + } + + if (nextObject is PGPSignatureList) { + signatures.addAll(nextObject as PGPSignatureList) + } + + if (nextObject is PGPSignature) { + signatures.add(nextObject as PGPSignature) + } + } + + pgpIn.close() + return signatures.toList() + } + + /** + * Determine the issuer key-id of a [PGPSignature]. This method first inspects the + * [org.bouncycastle.bcpg.sig.IssuerKeyID] subpacket of the signature and returns the key-id + * if present. If not, it inspects the [org.bouncycastle.bcpg.sig.IssuerFingerprint] packet + * and retrieves the key-id from the fingerprint. + * + * Otherwise, it returns 0. + * + * @param signature signature + * @return signatures issuing key id + */ + @JvmStatic + @Deprecated( + "Deprecated in favor of PGPSignature extension method.", + ReplaceWith("signature.issuerKeyId", "org.bouncycastle.extensions.issuerKeyId")) + fun determineIssuerKeyId(signature: PGPSignature): Long { + return signature.issuerKeyId + } + + /** + * Return the digest prefix of the signature as hex-encoded String. + * + * @param signature signature + * @return digest prefix + */ + @JvmStatic + fun getSignatureDigestPrefix(signature: PGPSignature): String { + return Hex.toHexString(signature.digestPrefix) + } + + @JvmStatic + @Deprecated( + "Deprecated in favor of PGPSignature extension method", + ReplaceWith( + "signature.wasIssuedBy(fingerprint)", "org.bouncycastle.extensions.wasIssuedBy")) + fun wasIssuedBy(fingerprint: ByteArray, signature: PGPSignature): Boolean { + return signature.wasIssuedBy(fingerprint) + } + + @JvmStatic + @Deprecated( + "Deprecated in favor of PGPSignature extension method", + ReplaceWith( + "signature.wasIssuedBy(fingerprint)", "org.bouncycastle.extensions.wasIssuedBy")) + fun wasIssuedBy(fingerprint: OpenPgpFingerprint, signature: PGPSignature): Boolean { + return signature.wasIssuedBy(fingerprint) + } + + /** + * Extract all signatures from the given
key
which were issued by + *
issuerKeyId
over
userId
. + * + * @param key public key + * @param userId user-id + * @param issuer issuer key-id + * @return (potentially empty) list of signatures + */ + @JvmStatic + fun getSignaturesOverUserIdBy( + key: PGPPublicKey, + userId: String, + issuer: Long + ): List { + val signatures = key.getSignaturesForID(userId) ?: return listOf() + return signatures.asSequence().filter { it.keyID == issuer }.toList() + } + + @JvmStatic + fun getDelegations(key: PGPPublicKeyRing): List { + return key.publicKey.keySignatures + .asSequence() + .filter { key.getPublicKey(it.keyID) == null } // Filter out back-sigs from subkeys + .toList() + } + + @JvmStatic + fun get3rdPartyCertificationsFor( + key: PGPPublicKeyRing, + userId: String + ): List { + return key.publicKey + .getSignaturesForID(userId) + .asSequence() + .filter { it.keyID != key.publicKey.keyID } // Filter out self-sigs + .toList() + } + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/AbstractSignatureBuilder.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/AbstractSignatureBuilder.kt new file mode 100644 index 00000000..eaf05df1 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/AbstractSignatureBuilder.kt @@ -0,0 +1,135 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.builder + +import java.util.function.Predicate +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPPrivateKey +import org.bouncycastle.openpgp.PGPPublicKey +import org.bouncycastle.openpgp.PGPSecretKey +import org.bouncycastle.openpgp.PGPSignature +import org.bouncycastle.openpgp.PGPSignatureGenerator +import org.pgpainless.PGPainless +import org.pgpainless.algorithm.HashAlgorithm +import org.pgpainless.algorithm.SignatureType +import org.pgpainless.algorithm.negotiation.HashAlgorithmNegotiator +import org.pgpainless.implementation.ImplementationFactory +import org.pgpainless.key.protection.SecretKeyRingProtector +import org.pgpainless.key.protection.UnlockSecretKey +import org.pgpainless.key.util.OpenPgpKeyAttributeUtil +import org.pgpainless.signature.subpackets.SignatureSubpackets +import org.pgpainless.signature.subpackets.SignatureSubpacketsHelper + +abstract class AbstractSignatureBuilder>( + protected val privateSigningKey: PGPPrivateKey, + protected val publicSigningKey: PGPPublicKey, + protected var _hashAlgorithm: HashAlgorithm, + protected var _signatureType: SignatureType, + protected val _hashedSubpackets: SignatureSubpackets, + protected val _unhashedSubpackets: SignatureSubpackets +) { + + protected abstract val signatureTypePredicate: Predicate + + init { + require(signatureTypePredicate.test(_signatureType)) { "Invalid signature type." } + } + + @Throws(PGPException::class) + protected constructor( + signatureType: SignatureType, + signingKey: PGPSecretKey, + protector: SecretKeyRingProtector, + hashAlgorithm: HashAlgorithm, + hashedSubpackets: SignatureSubpackets, + unhashedSubpackets: SignatureSubpackets + ) : this( + UnlockSecretKey.unlockSecretKey(signingKey, protector), + signingKey.publicKey, + hashAlgorithm, + signatureType, + hashedSubpackets, + unhashedSubpackets) + + @Throws(PGPException::class) + constructor( + signatureType: SignatureType, + signingKey: PGPSecretKey, + protector: SecretKeyRingProtector + ) : this( + signatureType, + signingKey, + protector, + negotiateHashAlgorithm(signingKey.publicKey), + SignatureSubpackets.createHashedSubpackets(signingKey.publicKey), + SignatureSubpackets.createEmptySubpackets()) + + @Throws(PGPException::class) + constructor( + signingKey: PGPSecretKey, + protector: SecretKeyRingProtector, + archetypeSignature: PGPSignature + ) : this( + SignatureType.requireFromCode(archetypeSignature.signatureType), + signingKey, + protector, + negotiateHashAlgorithm(signingKey.publicKey), + SignatureSubpackets.refreshHashedSubpackets(signingKey.publicKey, archetypeSignature), + SignatureSubpackets.refreshUnhashedSubpackets(archetypeSignature)) + + val hashAlgorithm = _hashAlgorithm + + fun overrideHashAlgorithm(hashAlgorithm: HashAlgorithm) = + apply { _hashAlgorithm = hashAlgorithm } as B + + /** + * Set the builders [SignatureType]. Note that only those types who are valid for the concrete + * subclass of this [AbstractSignatureBuilder] are allowed. Invalid choices result in an + * [IllegalArgumentException] to be thrown. + * + * @param type signature type + * @return builder + */ + fun setSignatureType(type: SignatureType) = + apply { + require(signatureTypePredicate.test(type)) { "Invalid signature type: $type" } + _signatureType = type + } + as B + + /** + * Build an instance of [PGPSignatureGenerator] initialized with the signing key and with hashed + * and unhashed subpackets. + * + * @return pgp signature generator + * @throws PGPException if the signature generator cannot be initialized + */ + @Throws(PGPException::class) + protected fun buildAndInitSignatureGenerator(): PGPSignatureGenerator = + PGPSignatureGenerator( + ImplementationFactory.getInstance() + .getPGPContentSignerBuilder( + publicSigningKey.algorithm, hashAlgorithm.algorithmId)) + .apply { + setUnhashedSubpackets(SignatureSubpacketsHelper.toVector(_unhashedSubpackets)) + setHashedSubpackets(SignatureSubpacketsHelper.toVector(_hashedSubpackets)) + init(_signatureType.code, privateSigningKey) + } + + companion object { + + /** + * Negotiate a [HashAlgorithm] to be used when creating the signature. + * + * @param publicKey signing public key + * @return hash algorithm + */ + @JvmStatic + fun negotiateHashAlgorithm(publicKey: PGPPublicKey): HashAlgorithm = + HashAlgorithmNegotiator.negotiateSignatureHashAlgorithm(PGPainless.getPolicy()) + .negotiateHashAlgorithm( + OpenPgpKeyAttributeUtil.getOrGuessPreferredHashAlgorithms(publicKey)) + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/DirectKeySelfSignatureBuilder.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/DirectKeySelfSignatureBuilder.kt new file mode 100644 index 00000000..c4d11ea9 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/DirectKeySelfSignatureBuilder.kt @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.builder + +import java.util.function.Predicate +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPSecretKey +import org.bouncycastle.openpgp.PGPSignature +import org.pgpainless.algorithm.SignatureType +import org.pgpainless.key.protection.SecretKeyRingProtector +import org.pgpainless.signature.subpackets.SelfSignatureSubpackets + +/** + * [AbstractSignatureBuilder] devoted to direct-key self-signatures. Direct-key self-signatures are + * calculated by a primary-key over itself. + */ +class DirectKeySelfSignatureBuilder : AbstractSignatureBuilder { + + override val signatureTypePredicate: Predicate + get() = Predicate { it == SignatureType.DIRECT_KEY } + + @Throws(PGPException::class) + constructor( + signingKey: PGPSecretKey, + protector: SecretKeyRingProtector, + archetypeSignature: PGPSignature + ) : super(signingKey, protector, archetypeSignature) + + @Throws(PGPException::class) + constructor( + signingKey: PGPSecretKey, + protector: SecretKeyRingProtector + ) : super(SignatureType.DIRECT_KEY, signingKey, protector) + + val hashedSubpackets: SelfSignatureSubpackets = _hashedSubpackets + val unhashedSubpackets: SelfSignatureSubpackets = _unhashedSubpackets + + fun applyCallback(callback: SelfSignatureSubpackets.Callback?) = apply { + callback?.let { + it.modifyHashedSubpackets(hashedSubpackets) + it.modifyUnhashedSubpackets(unhashedSubpackets) + } + } + + @Throws(PGPException::class) + fun build(): PGPSignature = + buildAndInitSignatureGenerator().let { it.generateCertification(publicSigningKey) } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/PrimaryKeyBindingSignatureBuilder.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/PrimaryKeyBindingSignatureBuilder.kt new file mode 100644 index 00000000..f34d37a7 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/PrimaryKeyBindingSignatureBuilder.kt @@ -0,0 +1,61 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.builder + +import java.util.function.Predicate +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPPublicKey +import org.bouncycastle.openpgp.PGPSecretKey +import org.bouncycastle.openpgp.PGPSignature +import org.pgpainless.algorithm.HashAlgorithm +import org.pgpainless.algorithm.SignatureType +import org.pgpainless.key.protection.SecretKeyRingProtector +import org.pgpainless.signature.subpackets.SelfSignatureSubpackets +import org.pgpainless.signature.subpackets.SignatureSubpackets + +/** + * [AbstractSignatureBuilder] subclass devoted to build primary-key binding-signatures. Those + * signatures (also called "back-signatures") are binding signatures issued by signing-capable + * subkeys. + */ +class PrimaryKeyBindingSignatureBuilder : + AbstractSignatureBuilder { + + override val signatureTypePredicate: Predicate + get() = Predicate { it == SignatureType.PRIMARYKEY_BINDING } + + @Throws(PGPException::class) + constructor( + signingSubkey: PGPSecretKey, + subkeyProtector: SecretKeyRingProtector + ) : super(SignatureType.PRIMARYKEY_BINDING, signingSubkey, subkeyProtector) + + @Throws(PGPException::class) + constructor( + signingSubkey: PGPSecretKey, + subkeyProtector: SecretKeyRingProtector, + hashAlgorithm: HashAlgorithm + ) : super( + SignatureType.PRIMARYKEY_BINDING, + signingSubkey, + subkeyProtector, + hashAlgorithm, + SignatureSubpackets.createHashedSubpackets(signingSubkey.publicKey), + SignatureSubpackets.createEmptySubpackets()) + + val hashedSubpackets: SelfSignatureSubpackets = _hashedSubpackets + val unhashedSubpackets: SelfSignatureSubpackets = _unhashedSubpackets + + fun applyCallback(callback: SelfSignatureSubpackets.Callback?) = apply { + callback?.let { + it.modifyHashedSubpackets(hashedSubpackets) + it.modifyUnhashedSubpackets(unhashedSubpackets) + } + } + + @Throws(PGPException::class) + fun build(primaryKey: PGPPublicKey): PGPSignature = + buildAndInitSignatureGenerator().generateCertification(primaryKey, publicSigningKey) +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/RevocationSignatureBuilder.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/RevocationSignatureBuilder.kt new file mode 100644 index 00000000..36589613 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/RevocationSignatureBuilder.kt @@ -0,0 +1,70 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.builder + +import java.util.function.Predicate +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPPublicKey +import org.bouncycastle.openpgp.PGPSecretKey +import org.bouncycastle.openpgp.PGPSignature +import org.pgpainless.algorithm.SignatureType +import org.pgpainless.key.protection.SecretKeyRingProtector +import org.pgpainless.signature.subpackets.RevocationSignatureSubpackets + +/** [AbstractSignatureBuilder] subclass devoted to revocation signatures. */ +class RevocationSignatureBuilder : AbstractSignatureBuilder { + + override val signatureTypePredicate: Predicate + get() = + Predicate { + it in + listOf( + SignatureType.KEY_REVOCATION, + SignatureType.SUBKEY_REVOCATION, + SignatureType.CERTIFICATION_REVOCATION) + } + + @Throws(PGPException::class) + constructor( + signatureType: SignatureType, + signingKey: PGPSecretKey, + protector: SecretKeyRingProtector + ) : super(signatureType, signingKey, protector) { + hashedSubpackets.setRevocable(false) + } + + val hashedSubpackets: RevocationSignatureSubpackets = _hashedSubpackets + val unhashedSubpackets: RevocationSignatureSubpackets = _unhashedSubpackets + + fun applyCallback(callback: RevocationSignatureSubpackets.Callback?) = apply { + callback?.let { + it.modifyHashedSubpackets(hashedSubpackets) + it.modifyUnhashedSubpackets(unhashedSubpackets) + } + } + + @Throws(PGPException::class) + fun build(revokeeKey: PGPPublicKey): PGPSignature = + buildAndInitSignatureGenerator().let { + if (_signatureType == SignatureType.KEY_REVOCATION) { + require(revokeeKey.isMasterKey) { + "Signature type is KEY_REVOCATION, but provided revokee does not appear to be a primary key." + } + it.generateCertification(revokeeKey) + } else { + it.generateCertification(publicSigningKey, revokeeKey) + } + } + + @Throws(PGPException::class) + fun build(revokeeUserId: CharSequence): PGPSignature = + buildAndInitSignatureGenerator() + .also { + require(_signatureType == SignatureType.CERTIFICATION_REVOCATION) { + "Signature type is != CERTIFICATION_REVOCATION." + } + } + .generateCertification(revokeeUserId.toString(), publicSigningKey) +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/SelfSignatureBuilder.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/SelfSignatureBuilder.kt new file mode 100644 index 00000000..370479c8 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/SelfSignatureBuilder.kt @@ -0,0 +1,69 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.builder + +import java.util.function.Predicate +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPSecretKey +import org.bouncycastle.openpgp.PGPSignature +import org.bouncycastle.openpgp.PGPUserAttributeSubpacketVector +import org.pgpainless.algorithm.SignatureType +import org.pgpainless.key.protection.SecretKeyRingProtector +import org.pgpainless.signature.subpackets.SelfSignatureSubpackets + +/** + * [AbstractSignatureBuilder] devoted to all types of self-certifications. Self-certifications are + * certifications calculated by a primary key over its own user-ids. + */ +class SelfSignatureBuilder : AbstractSignatureBuilder { + override val signatureTypePredicate: Predicate + get() = + Predicate { + it in + listOf( + SignatureType.GENERIC_CERTIFICATION, + SignatureType.NO_CERTIFICATION, + SignatureType.CASUAL_CERTIFICATION, + SignatureType.POSITIVE_CERTIFICATION) + } + + @Throws(PGPException::class) + constructor( + signingKey: PGPSecretKey, + protector: SecretKeyRingProtector + ) : super(SignatureType.GENERIC_CERTIFICATION, signingKey, protector) + + @Throws(PGPException::class) + constructor( + signatureType: SignatureType, + signingKey: PGPSecretKey, + protector: SecretKeyRingProtector + ) : super(signatureType, signingKey, protector) + + @Throws(PGPException::class) + constructor( + primaryKey: PGPSecretKey, + primaryKeyProtector: SecretKeyRingProtector, + oldCertification: PGPSignature + ) : super(primaryKey, primaryKeyProtector, oldCertification) + + val hashedSubpackets: SelfSignatureSubpackets = _hashedSubpackets + val unhashedSubpackets: SelfSignatureSubpackets = _unhashedSubpackets + + fun applyCallback(callback: SelfSignatureSubpackets.Callback?) = apply { + callback?.let { + it.modifyHashedSubpackets(hashedSubpackets) + it.modifyUnhashedSubpackets(unhashedSubpackets) + } + } + + @Throws(PGPException::class) + fun build(userId: CharSequence): PGPSignature = + buildAndInitSignatureGenerator().generateCertification(userId.toString(), publicSigningKey) + + @Throws(PGPException::class) + fun build(userAttributes: PGPUserAttributeSubpacketVector): PGPSignature = + buildAndInitSignatureGenerator().generateCertification(userAttributes, publicSigningKey) +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/SubkeyBindingSignatureBuilder.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/SubkeyBindingSignatureBuilder.kt new file mode 100644 index 00000000..6e2694e3 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/SubkeyBindingSignatureBuilder.kt @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.builder + +import java.util.function.Predicate +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPPublicKey +import org.bouncycastle.openpgp.PGPSecretKey +import org.bouncycastle.openpgp.PGPSignature +import org.pgpainless.algorithm.HashAlgorithm +import org.pgpainless.algorithm.SignatureType +import org.pgpainless.key.protection.SecretKeyRingProtector +import org.pgpainless.signature.subpackets.SelfSignatureSubpackets +import org.pgpainless.signature.subpackets.SignatureSubpackets + +/** + * [AbstractSignatureBuilder] devoted to generating subkey binding signatures. A subkey binding + * signature is calculated by a primary key over a subkey. + */ +class SubkeyBindingSignatureBuilder : AbstractSignatureBuilder { + + override val signatureTypePredicate: Predicate + get() = Predicate { it == SignatureType.SUBKEY_BINDING } + + @Throws(PGPException::class) + constructor( + signingKey: PGPSecretKey, + protector: SecretKeyRingProtector + ) : super(SignatureType.SUBKEY_BINDING, signingKey, protector) + + @Throws(PGPException::class) + constructor( + signingKey: PGPSecretKey, + protector: SecretKeyRingProtector, + hashAlgorithm: HashAlgorithm + ) : super( + SignatureType.SUBKEY_BINDING, + signingKey, + protector, + hashAlgorithm, + SignatureSubpackets.createHashedSubpackets(signingKey.publicKey), + SignatureSubpackets.createEmptySubpackets()) + + @Throws(PGPException::class) + constructor( + signingKey: PGPSecretKey, + protector: SecretKeyRingProtector, + oldSubkeyBinding: PGPSignature + ) : super( + signingKey, + protector, + oldSubkeyBinding.also { + require(it.signatureType == SignatureType.SUBKEY_BINDING.code) { + "Invalid signature type." + } + }) + + val hashedSubpackets: SelfSignatureSubpackets = _hashedSubpackets + val unhashedSubpackets: SelfSignatureSubpackets = _unhashedSubpackets + + fun applyCallback(callback: SelfSignatureSubpackets.Callback?) = apply { + callback?.let { + it.modifyHashedSubpackets(hashedSubpackets) + it.modifyUnhashedSubpackets(unhashedSubpackets) + } + } + + @Throws(PGPException::class) + fun build(subkey: PGPPublicKey): PGPSignature = + buildAndInitSignatureGenerator().generateCertification(publicSigningKey, subkey) +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/ThirdPartyCertificationSignatureBuilder.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/ThirdPartyCertificationSignatureBuilder.kt new file mode 100644 index 00000000..be45dc8a --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/ThirdPartyCertificationSignatureBuilder.kt @@ -0,0 +1,117 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.builder + +import java.util.function.Predicate +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPPublicKeyRing +import org.bouncycastle.openpgp.PGPSecretKey +import org.bouncycastle.openpgp.PGPSignature +import org.bouncycastle.openpgp.PGPUserAttributeSubpacketVector +import org.pgpainless.algorithm.SignatureType +import org.pgpainless.key.protection.SecretKeyRingProtector +import org.pgpainless.signature.subpackets.CertificationSubpackets + +/** + * Certification signature builder used to certify other users keys. A third-party certification is + * calculated by the primary key of the issuer certificate, over a user-id on a third-party + * certificate. + */ +class ThirdPartyCertificationSignatureBuilder : + AbstractSignatureBuilder { + override val signatureTypePredicate: Predicate + get() = + Predicate { + it in + listOf( + SignatureType.GENERIC_CERTIFICATION, + SignatureType.NO_CERTIFICATION, + SignatureType.CASUAL_CERTIFICATION, + SignatureType.POSITIVE_CERTIFICATION) + } + + /** + * Create a new certification signature builder. This constructor uses + * [SignatureType.GENERIC_CERTIFICATION] as signature type. + * + * @param signingKey our own certification key + * @param protector protector to unlock the certification key + * @throws WrongPassphraseException in case of a wrong passphrase + */ + @Throws(PGPException::class) + constructor( + signingKey: PGPSecretKey, + protector: SecretKeyRingProtector + ) : super(SignatureType.GENERIC_CERTIFICATION, signingKey, protector) + + /** + * Create a new certification signature builder. + * + * @param signatureType type of certification + * @param signingKey our own certification key + * @param protector protector to unlock the certification key + * @throws WrongPassphraseException in case of a wrong passphrase + */ + @Throws(PGPException::class) + constructor( + signatureType: SignatureType, + signingKey: PGPSecretKey, + protector: SecretKeyRingProtector + ) : super(signatureType, signingKey, protector) + + /** + * Create a new certification signature builder. + * + * @param signingKey our own certification key + * @param protector protector to unlock the certification key + * @param archetypeSignature signature to use as a template for the new signature + * @throws WrongPassphraseException in case of a wrong passphrase + */ + @Throws(PGPException::class) + constructor( + signingKey: PGPSecretKey, + protector: SecretKeyRingProtector, + archetypeSignature: PGPSignature + ) : super(signingKey, protector, archetypeSignature) + + val hashedSubpackets: CertificationSubpackets = _hashedSubpackets + val unhashedSubpackets: CertificationSubpackets = _unhashedSubpackets + + fun applyCallback(callback: CertificationSubpackets.Callback?) = apply { + callback?.let { + it.modifyHashedSubpackets(hashedSubpackets) + it.modifyUnhashedSubpackets(unhashedSubpackets) + } + } + + /** + * Create a certification signature for the given user-id and the given third-party certificate. + * + * @param certificate third-party certificate + * @param userId user-id to certify + * @return signature + * @throws PGPException if the signature generator cannot be initialized + */ + @Throws(PGPException::class) + fun build(certificate: PGPPublicKeyRing, userId: CharSequence): PGPSignature = + buildAndInitSignatureGenerator() + .generateCertification(userId.toString(), certificate.publicKey) + + /** + * Create a certification signature for the given user attribute and the given third-party + * certificate. + * + * @param certificate third-party certificate + * @param userAttribute user-attributes to certify + * @return signature + * @throws PGPException if the signature generator cannot be initialized + */ + @Throws(PGPException::class) + fun build( + certificate: PGPPublicKeyRing, + userAttribute: PGPUserAttributeSubpacketVector + ): PGPSignature = + buildAndInitSignatureGenerator().generateCertification(userAttribute, certificate.publicKey) +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/ThirdPartyDirectKeySignatureBuilder.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/ThirdPartyDirectKeySignatureBuilder.kt new file mode 100644 index 00000000..32d677d5 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/ThirdPartyDirectKeySignatureBuilder.kt @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.builder + +import java.util.function.Predicate +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPPublicKey +import org.bouncycastle.openpgp.PGPPublicKeyRing +import org.bouncycastle.openpgp.PGPSecretKey +import org.bouncycastle.openpgp.PGPSignature +import org.pgpainless.algorithm.SignatureType +import org.pgpainless.key.protection.SecretKeyRingProtector +import org.pgpainless.signature.subpackets.CertificationSubpackets + +/** + * [AbstractSignatureBuilder] subclass devoted to generating direct-key signatures over primary keys + * of third-party certificates. Such signatures are also sometimes referred to as "delegations", + * i.e. in the context of the Web-of-Trust. + */ +class ThirdPartyDirectKeySignatureBuilder : + AbstractSignatureBuilder { + + override val signatureTypePredicate: Predicate + get() = Predicate { it == SignatureType.DIRECT_KEY } + + @Throws(PGPException::class) + constructor( + signingKey: PGPSecretKey, + protector: SecretKeyRingProtector + ) : super(SignatureType.DIRECT_KEY, signingKey, protector) + + @Throws(PGPException::class) + constructor( + signingKey: PGPSecretKey, + protector: SecretKeyRingProtector, + archetypeSignature: PGPSignature + ) : super(signingKey, protector, archetypeSignature) + + val hashedSubpackets: CertificationSubpackets = _hashedSubpackets + val unhashedSubpackets: CertificationSubpackets = _unhashedSubpackets + + fun applyCallback(callback: CertificationSubpackets.Callback?) = apply { + callback?.let { + it.modifyHashedSubpackets(hashedSubpackets) + it.modifyUnhashedSubpackets(unhashedSubpackets) + } + } + + @Throws(PGPException::class) + fun build(certificate: PGPPublicKeyRing): PGPSignature = build(certificate.publicKey) + + @Throws(PGPException::class) + fun build(certifiedKey: PGPPublicKey): PGPSignature = + buildAndInitSignatureGenerator().generateCertification(certifiedKey) +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/UniversalSignatureBuilder.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/UniversalSignatureBuilder.kt new file mode 100644 index 00000000..a1aa57b2 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/UniversalSignatureBuilder.kt @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.builder + +import java.util.function.Predicate +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPSecretKey +import org.bouncycastle.openpgp.PGPSignature +import org.bouncycastle.openpgp.PGPSignatureGenerator +import org.pgpainless.algorithm.SignatureType +import org.pgpainless.key.protection.SecretKeyRingProtector +import org.pgpainless.signature.subpackets.SignatureSubpackets + +/** + * Signature builder without restrictions on subpacket contents. Instead of providing a "build" + * method, this builder offers the user to decide on their own, how to generate the signature by + * exposing the [PGPSignatureGenerator] via [signatureGenerator]. + */ +class UniversalSignatureBuilder : AbstractSignatureBuilder { + + override val signatureTypePredicate: Predicate + get() = Predicate { true } + + @Throws(PGPException::class) + constructor( + signatureType: SignatureType, + signingKey: PGPSecretKey, + protector: SecretKeyRingProtector + ) : super(signatureType, signingKey, protector) + + @Throws(PGPException::class) + constructor( + signingKey: PGPSecretKey, + protector: SecretKeyRingProtector, + archetypeSignature: PGPSignature + ) : super(signingKey, protector, archetypeSignature) + + val hashedSubpackets: SignatureSubpackets = _hashedSubpackets + val unhashedSubpackets: SignatureSubpackets = _unhashedSubpackets + + fun applyCallback(callback: SignatureSubpackets.Callback?) = apply { + callback?.let { + it.modifyHashedSubpackets(hashedSubpackets) + it.modifyUnhashedSubpackets(unhashedSubpackets) + } + } + + val signatureGenerator: PGPSignatureGenerator + @Throws(PGPException::class) get() = buildAndInitSignatureGenerator() +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/CertificateValidator.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/CertificateValidator.kt new file mode 100644 index 00000000..83b7e54e --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/CertificateValidator.kt @@ -0,0 +1,315 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.consumer + +import java.io.InputStream +import java.util.* +import openpgp.openPgpKeyId +import org.bouncycastle.openpgp.PGPPublicKey +import org.bouncycastle.openpgp.PGPPublicKeyRing +import org.bouncycastle.openpgp.PGPSignature +import org.pgpainless.PGPainless +import org.pgpainless.algorithm.KeyFlag +import org.pgpainless.algorithm.SignatureType +import org.pgpainless.bouncycastle.extensions.issuerKeyId +import org.pgpainless.exception.SignatureValidationException +import org.pgpainless.key.util.KeyRingUtils +import org.pgpainless.policy.Policy +import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil +import org.slf4j.LoggerFactory + +/** + * A collection of static methods that validate signing certificates (public keys) and verify + * signature correctness. + */ +class CertificateValidator { + + companion object { + + @JvmStatic private val LOGGER = LoggerFactory.getLogger(CertificateValidator::class.java) + + /** + * Check if the signing key was eligible to create the provided signature. + * + * That entails: + * - Check, if the primary key is being revoked via key-revocation signatures. + * - Check, if the keys user-ids are revoked or not bound. + * - Check, if the signing subkey is revoked or expired. + * - Check, if the signing key is not capable of signing + * + * @param signature signature + * @param signingKeyRing signing key ring + * @param policy validation policy + * @return true if the signing key was eligible to create the signature + * @throws SignatureValidationException in case of a validation constraint violation + */ + @JvmStatic + @Throws(SignatureValidationException::class) + fun validateCertificate( + signature: PGPSignature, + signingKeyRing: PGPPublicKeyRing, + policy: Policy = PGPainless.getPolicy() + ): Boolean { + val signingSubkey: PGPPublicKey = + signingKeyRing.getPublicKey(signature.issuerKeyId) + ?: throw SignatureValidationException( + "Provided key ring does not contain a subkey with id ${signature.issuerKeyId.openPgpKeyId()}.") + val primaryKey = signingKeyRing.publicKey!! + val directKeyAndRevSigs = mutableListOf() + val rejections = mutableMapOf() + // revocations + primaryKey + .getSignaturesOfType(SignatureType.KEY_REVOCATION.code) + .asSequence() + .filter { + it.issuerKeyId == primaryKey.keyID + } // We do not support external rev keys + .forEach { + try { + if (SignatureVerifier.verifyKeyRevocationSignature( + it, primaryKey, policy, signature.creationTime)) { + directKeyAndRevSigs.add(it) + } + } catch (e: SignatureValidationException) { + rejections[it] = e + LOGGER.debug("Rejecting key revocation signature: ${e.message}", e) + } + } + + // direct-key sigs + primaryKey + .getSignaturesOfType(SignatureType.DIRECT_KEY.code) + .asSequence() + .filter { it.issuerKeyId == primaryKey.keyID } + .forEach { + try { + if (SignatureVerifier.verifyDirectKeySignature( + it, primaryKey, policy, signature.creationTime)) { + directKeyAndRevSigs.add(it) + } + } catch (e: SignatureValidationException) { + rejections[it] = e + LOGGER.debug("Rejecting key signature: ${e.message}, e") + } + } + + directKeyAndRevSigs.sortWith( + SignatureValidityComparator(SignatureCreationDateComparator.Order.NEW_TO_OLD)) + if (directKeyAndRevSigs.isNotEmpty()) { + if (directKeyAndRevSigs[0].signatureType == SignatureType.KEY_REVOCATION.code) { + throw SignatureValidationException("Primary key has been revoked.") + } + } + + // UserID signatures + val userIdSignatures = mutableMapOf>() + KeyRingUtils.getUserIdsIgnoringInvalidUTF8(primaryKey).forEach { userId -> + buildList { + primaryKey + .getSignaturesForID(userId) + .asSequence() + .filter { it.issuerKeyId == primaryKey.keyID } + .forEach { uidSig -> + try { + if (SignatureVerifier.verifySignatureOverUserId( + userId, + uidSig, + primaryKey, + policy, + signature.creationTime)) { + add(uidSig) + } + } catch (e: SignatureValidationException) { + rejections[uidSig] = e + LOGGER.debug("Rejecting user-id signature: ${e.message}", e) + } + } + } + .sortedWith( + SignatureValidityComparator( + SignatureCreationDateComparator.Order.NEW_TO_OLD)) + .let { userIdSignatures[userId] = it } + } + + val hasAnyUserIds = userIdSignatures.isNotEmpty() + val isAnyUserIdValid = + userIdSignatures.any { entry -> + entry.value.isNotEmpty() && + entry.value[0].signatureType != SignatureType.CERTIFICATION_REVOCATION.code + } + + if (hasAnyUserIds && !isAnyUserIdValid) { + throw SignatureValidationException("No valid user-id found.", rejections) + } + + // Specific signer user-id + if (policy.signerUserIdValidationLevel == Policy.SignerUserIdValidationLevel.STRICT) { + SignatureSubpacketsUtil.getSignerUserID(signature)?.let { + if (userIdSignatures[it.id] == null || userIdSignatures[it.id]!!.isEmpty()) { + throw SignatureValidationException( + "Signature was allegedly made by user-id '${it.id}'," + + " but we have no valid signatures for that on the certificate.") + } + + if (userIdSignatures[it.id]!![0].signatureType == + SignatureType.CERTIFICATION_REVOCATION.code) { + throw SignatureValidationException( + "Signature was made with user-id '${it.id}' which is revoked.") + } + } + } + + if (signingSubkey.keyID == primaryKey.keyID) { // signing key is primary key + if (directKeyAndRevSigs.isNotEmpty()) { + val directKeySig = directKeyAndRevSigs[0]!! + val flags = SignatureSubpacketsUtil.getKeyFlags(directKeySig) + if (flags != null && KeyFlag.hasKeyFlag(flags.flags, KeyFlag.SIGN_DATA)) { + return true + } + } + // Reject sigs by non-signing keys + if (userIdSignatures.none { (_, sigs) -> + sigs.any { + SignatureSubpacketsUtil.getKeyFlags(it)?.let { f -> + KeyFlag.hasKeyFlag(f.flags, KeyFlag.SIGN_DATA) + } == true + } + }) { + throw SignatureValidationException( + "Signature was generated by non-signing key.") + } + } else { // signing key is subkey + val subkeySigs = mutableListOf() + signingSubkey + .getSignaturesOfType(SignatureType.SUBKEY_REVOCATION.code) + .asSequence() + .filter { it.issuerKeyId == primaryKey.keyID } + .forEach { + try { + if (SignatureVerifier.verifySubkeyBindingRevocation( + it, primaryKey, signingSubkey, policy, signature.creationTime)) { + subkeySigs.add(it) + } + } catch (e: SignatureValidationException) { + rejections[it] = e + LOGGER.debug("Rejecting subkey revocation signature: ${e.message}", e) + } + } + + signingSubkey + .getSignaturesOfType(SignatureType.SUBKEY_BINDING.code) + .asSequence() + .forEach { + try { + if (SignatureVerifier.verifySubkeyBindingSignature( + it, primaryKey, signingSubkey, policy, signature.creationTime)) { + subkeySigs.add(it) + } + } catch (e: SignatureValidationException) { + rejections[it] = e + LOGGER.debug("Rejecting subkey binding signature: ${e.message}", e) + } + } + + subkeySigs.sortWith( + SignatureValidityComparator(SignatureCreationDateComparator.Order.NEW_TO_OLD)) + if (subkeySigs.isEmpty()) { + throw SignatureValidationException("Subkey is not bound.", rejections) + } + + if (subkeySigs[0].signatureType == SignatureType.SUBKEY_REVOCATION.code) { + throw SignatureValidationException("Subkey is revoked.") + } + + val keyFlags = SignatureSubpacketsUtil.getKeyFlags(subkeySigs[0]) + if (keyFlags == null || !KeyFlag.hasKeyFlag(keyFlags.flags, KeyFlag.SIGN_DATA)) { + throw SignatureValidationException( + "Signature was made by key which is not capable of signing (no keyflag).") + } + } + return true + } + + /** + * Validate the given signing key and then verify the given signature while parsing out the + * signed data. Uninitialized means that no signed data has been read and the hash + * generators state has not yet been updated. + * + * @param signature uninitialized signature + * @param signedData input stream containing signed data + * @param signingKeyRing key ring containing signing key + * @param policy validation policy + * @param validationDate date of validation + * @return true if the signature is valid, false otherwise + * @throws SignatureValidationException for validation constraint violations + */ + @JvmStatic + @Throws(SignatureValidationException::class) + fun validateCertificateAndVerifyUninitializedSignature( + signature: PGPSignature, + signedData: InputStream, + signingKeyRing: PGPPublicKeyRing, + policy: Policy, + referenceTime: Date = signature.creationTime + ): Boolean { + return validateCertificate(signature, signingKeyRing, policy) && + SignatureVerifier.verifyUninitializedSignature( + signature, + signedData, + signingKeyRing.getPublicKey(signature.issuerKeyId)!!, + policy, + referenceTime) + } + + /** + * Validate the signing key and the given initialized signature. Initialized means that the + * signatures hash generator has already been updated by reading the signed data completely. + * + * @param signature initialized signature + * @param verificationKeys key ring containing the verification key + * @param policy validation policy + * @return true if the signature is valid, false otherwise + * @throws SignatureValidationException in case of a validation constraint violation + */ + @JvmStatic + @Throws(SignatureValidationException::class) + fun validateCertificateAndVerifyInitializedSignature( + signature: PGPSignature, + verificationKeys: PGPPublicKeyRing, + policy: Policy + ): Boolean { + return validateCertificate(signature, verificationKeys, policy) && + SignatureVerifier.verifyInitializedSignature( + signature, + verificationKeys.getPublicKey(signature.issuerKeyId), + policy, + signature.creationTime) + } + + /** + * Validate the signing key certificate and the given [OnePassSignatureCheck]. + * + * @param onePassSignature corresponding one-pass-signature + * @param policy policy + * @return true if the certificate is valid and the signature is correct, false otherwise. + * @throws SignatureValidationException in case of a validation error + */ + @JvmStatic + @Throws(SignatureValidationException::class) + fun validateCertificateAndVerifyOnePassSignature( + onePassSignature: OnePassSignatureCheck, + policy: Policy + ): Boolean { + return validateCertificate( + onePassSignature.signature!!, onePassSignature.verificationKeys, policy) && + SignatureVerifier.verifyOnePassSignature( + onePassSignature.signature!!, + onePassSignature.verificationKeys.getPublicKey( + onePassSignature.signature!!.issuerKeyId), + onePassSignature, + policy) + } + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/OnePassSignatureCheck.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/OnePassSignatureCheck.kt new file mode 100644 index 00000000..4a89e0b2 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/OnePassSignatureCheck.kt @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.consumer + +import org.bouncycastle.openpgp.PGPOnePassSignature +import org.bouncycastle.openpgp.PGPPublicKeyRing +import org.bouncycastle.openpgp.PGPSignature +import org.pgpainless.key.SubkeyIdentifier + +/** + * Tuple-class that bundles together a [PGPOnePassSignature] object, a [PGPPublicKeyRing] destined + * to verify the signature, the [PGPSignature] itself and a record of whether the signature was + * verified. + * + * @param onePassSignature the one-pass-signature packet + * @param verificationKeys certificate containing the signing subkey + * @param signature the signature packet + */ +data class OnePassSignatureCheck( + val onePassSignature: PGPOnePassSignature, + val verificationKeys: PGPPublicKeyRing, + var signature: PGPSignature? = null +) { + + /** + * Return an identifier for the signing key. + * + * @return signing key fingerprint + */ + val signingKey: SubkeyIdentifier + get() = SubkeyIdentifier(verificationKeys, onePassSignature.keyID) +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/README.md b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/README.md similarity index 95% rename from pgpainless-core/src/main/java/org/pgpainless/signature/README.md rename to pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/README.md index 5af3ccd9..b39097ec 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/README.md +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/README.md @@ -14,7 +14,7 @@ therefore let me quickly outline some of its challenges for you: A signature is either valid or it is not. However, signature validity goes beyond merely checking the cryptographic correctness like BouncyCastle does. -A signature that is correct can still be invalid, eg. if it is past its expiry date +A signature that is correct can still be invalid, e.g. if it is past its expiry date or the key that issued the signature got revoked or is simply not a signing key in the first place. All the little criteria like "is not expired", "has a hashed signature creation time subpacket", diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureCheck.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureCheck.kt new file mode 100644 index 00000000..15564773 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureCheck.kt @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.consumer + +import org.bouncycastle.openpgp.PGPKeyRing +import org.bouncycastle.openpgp.PGPSignature +import org.pgpainless.key.SubkeyIdentifier + +/** + * Tuple-class which bundles together a signature, the signing key that created the signature, an + * identifier of the signing key and a record of whether the signature was verified. + * + * @param signature OpenPGP signature + * @param signingKeyIdentifier identifier pointing to the exact signing key which was used to create + * the signature + * @param signingKeyRing certificate or key ring that contains the signing key that created the + * signature + */ +data class SignatureCheck( + val signature: PGPSignature, + val signingKeyRing: PGPKeyRing, + val signingKeyIdentifier: SubkeyIdentifier +) {} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureCreationDateComparator.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureCreationDateComparator.kt new file mode 100644 index 00000000..a913bf32 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureCreationDateComparator.kt @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.consumer + +import org.bouncycastle.openpgp.PGPSignature + +/** + * Create a new comparator which sorts signatures according to the passed ordering. + * + * @param order ordering + */ +class SignatureCreationDateComparator(private val order: Order = Order.OLD_TO_NEW) : + Comparator { + + enum class Order { + /** Oldest signatures first. */ + OLD_TO_NEW, + /** Newest signatures first. */ + NEW_TO_OLD + } + + override fun compare(one: PGPSignature, two: PGPSignature): Int { + return when (order) { + Order.OLD_TO_NEW -> one.creationTime.compareTo(two.creationTime) + Order.NEW_TO_OLD -> two.creationTime.compareTo(one.creationTime) + } + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignaturePicker.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignaturePicker.kt new file mode 100644 index 00000000..5952003e --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignaturePicker.kt @@ -0,0 +1,383 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.consumer + +import java.util.Date +import org.bouncycastle.openpgp.PGPKeyRing +import org.bouncycastle.openpgp.PGPPublicKey +import org.bouncycastle.openpgp.PGPSignature +import org.pgpainless.algorithm.SignatureType +import org.pgpainless.bouncycastle.extensions.getPublicKeyFor +import org.pgpainless.bouncycastle.extensions.isExpired +import org.pgpainless.bouncycastle.extensions.wasIssuedBy +import org.pgpainless.exception.SignatureValidationException +import org.pgpainless.policy.Policy + +/** + * Pick signatures from keys. + * + * The format of a V4 OpenPGP key is: + * + * Primary-Key [Revocation Self Signature] [Direct Key Signature...] User ID [Signature ...] [User + * ID [Signature ...] ...] [User Attribute [Signature ...] ...] [[Subkey + * [Binding-Signature-Revocation] Primary-Key-Binding-Signature] ...] + */ +class SignaturePicker { + + companion object { + + /** + * Pick the at validation date most recent valid key revocation signature. If there are hard + * revocation signatures, the latest hard revocation sig is picked, even if it was created + * after validationDate or if it is already expired. + * + * @param keyRing key ring + * @param policy policy + * @param referenceTime date of signature validation + * @return most recent, valid key revocation signature + */ + @JvmStatic + fun pickCurrentRevocationSelfSignature( + keyRing: PGPKeyRing, + policy: Policy, + referenceTime: Date + ): PGPSignature? { + val primaryKey = keyRing.publicKey + return getSortedSignaturesOfType(primaryKey, SignatureType.KEY_REVOCATION).lastOrNull { + return@lastOrNull try { + SignatureVerifier.verifyKeyRevocationSignature( + it, primaryKey, policy, referenceTime) + true // valid + } catch (e: SignatureValidationException) { + false // not valid + } + } + } + + /** + * Pick the at validationDate most recent, valid direct key signature. This method might + * return null, if there is no direct key self-signature which is valid at validationDate. + * + * @param keyRing key ring + * @param policy policy + * @param referenceTime validation date + * @return direct-key self-signature + */ + @JvmStatic + fun pickCurrentDirectKeySelfSignature( + keyRing: PGPKeyRing, + policy: Policy, + referenceTime: Date + ): PGPSignature? { + val primaryKey = keyRing.publicKey + return pickCurrentDirectKeySignature(primaryKey, primaryKey, policy, referenceTime) + } + + @JvmStatic + fun pickCurrentDirectKeySignature( + signingKey: PGPPublicKey, + signedKey: PGPPublicKey, + policy: Policy, + referenceTime: Date + ): PGPSignature? { + return getSortedSignaturesOfType(signedKey, SignatureType.DIRECT_KEY).lastOrNull { + return@lastOrNull try { + SignatureVerifier.verifyDirectKeySignature( + it, signingKey, signedKey, policy, referenceTime) + true + } catch (e: SignatureValidationException) { + false + } + } + } + + /** + * Pick the at validationDate latest direct key signature. This method might return an + * expired signature. If there are more than one direct-key signature, and some of those are + * not expired, the latest non-expired yet already effective direct-key signature will be + * returned. + * + * @param keyRing key ring + * @param policy policy + * @param referenceTime validation date + * @return latest direct key signature + */ + @JvmStatic + fun pickLatestDirectKeySignature( + keyRing: PGPKeyRing, + policy: Policy, + referenceTime: Date + ): PGPSignature? { + return pickLatestDirectKeySignature( + keyRing.publicKey, keyRing.publicKey, policy, referenceTime) + } + + /** + * Pick the at validationDate latest direct key signature made by signingKey on signedKey. + * This method might return an expired signature. If a non-expired direct-key signature + * exists, the latest non-expired yet already effective direct-key signature will be + * returned. + * + * @param signingKey signing key (key that made the sig) + * @param signedKey signed key (key that carries the sig) + * @param policy policy + * @param referenceTime date of validation + * @return latest direct key sig + */ + @JvmStatic + fun pickLatestDirectKeySignature( + signingKey: PGPPublicKey, + signedKey: PGPPublicKey, + policy: Policy, + referenceTime: Date + ): PGPSignature? { + var latest: PGPSignature? = null + return getSortedSignaturesOfType(signedKey, SignatureType.DIRECT_KEY).lastOrNull { + try { + SignatureValidator.signatureIsOfType(SignatureType.DIRECT_KEY).verify(it) + SignatureValidator.signatureStructureIsAcceptable(signingKey, policy).verify(it) + SignatureValidator.signatureIsAlreadyEffective(referenceTime).verify(it) + if (latest != null && !latest!!.isExpired(referenceTime)) { + SignatureValidator.signatureIsNotYetExpired(referenceTime).verify(it) + } + SignatureValidator.correctSignatureOverKey(signingKey, signedKey).verify(it) + latest = it + true + } catch (e: SignatureValidationException) { + false + } + } + } + + /** + * Pick the at validationDate most recent, valid user-id revocation signature. If there are + * hard revocation signatures, the latest hard revocation sig is picked, even if it was + * created after validationDate or if it is already expired. + * + * @param keyRing key ring + * @param userId user-Id that gets revoked + * @param policy policy + * @param referenceTime validation date + * @return revocation signature + */ + @JvmStatic + fun pickCurrentUserIdRevocationSignature( + keyRing: PGPKeyRing, + userId: CharSequence, + policy: Policy, + referenceTime: Date + ): PGPSignature? { + val primaryKey = keyRing.publicKey + return getSortedSignaturesOfType(primaryKey, SignatureType.CERTIFICATION_REVOCATION) + .lastOrNull { + keyRing.getPublicKeyFor(it) + ?: return@lastOrNull false // signature made by external key. skip. + return@lastOrNull try { + SignatureVerifier.verifyUserIdRevocation( + userId.toString(), it, primaryKey, policy, referenceTime) + true + } catch (e: SignatureValidationException) { + false // signature not valid + } + } + } + + /** + * Pick the at validationDate latest, valid certification self-signature for the given + * user-id. This method might return null, if there is no certification self signature for + * that user-id which is valid at validationDate. + * + * @param keyRing keyring + * @param userId userid + * @param policy policy + * @param referenceTime validation date + * @return user-id certification + */ + @JvmStatic + fun pickCurrentUserIdCertificationSignature( + keyRing: PGPKeyRing, + userId: CharSequence, + policy: Policy, + referenceTime: Date + ): PGPSignature? { + val primaryKey = keyRing.publicKey + return primaryKey + .getSignaturesForID(userId.toString()) + .asSequence() + .sortedWith(SignatureCreationDateComparator()) + .lastOrNull { + return@lastOrNull it.wasIssuedBy(primaryKey) && + try { + SignatureVerifier.verifyUserIdCertification( + userId.toString(), it, primaryKey, policy, referenceTime) + true + } catch (e: SignatureValidationException) { + false + } + } + } + + /** + * Pick the at validationDate latest certification self-signature for the given user-id. + * This method might return an expired signature. If a non-expired user-id certification + * signature exists, the latest non-expired yet already effective user-id certification + * signature for the given user-id will be returned. + * + * @param keyRing keyring + * @param userId userid + * @param policy policy + * @param referenceTime validation date + * @return user-id certification + */ + @JvmStatic + fun pickLatestUserIdCertificationSignature( + keyRing: PGPKeyRing, + userId: CharSequence, + policy: Policy, + referenceTime: Date + ): PGPSignature? { + val primaryKey = keyRing.publicKey + return primaryKey + .getSignaturesForID(userId.toString()) + .asSequence() + .sortedWith(SignatureCreationDateComparator()) + .lastOrNull { + return@lastOrNull try { + SignatureValidator.wasPossiblyMadeByKey(primaryKey).verify(it) + SignatureValidator.signatureIsCertification().verify(it) + SignatureValidator.signatureStructureIsAcceptable(primaryKey, policy) + .verify(it) + SignatureValidator.signatureIsAlreadyEffective(referenceTime).verify(it) + SignatureValidator.correctSignatureOverUserId( + userId.toString(), primaryKey, primaryKey) + .verify(it) + true + } catch (e: SignatureValidationException) { + false + } + } + } + + /** + * Pick the at validationDate most recent, valid subkey revocation signature. If there are + * hard revocation signatures, the latest hard revocation sig is picked, even if it was + * created after validationDate or if it is already expired. + * + * @param keyRing keyring + * @param subkey subkey + * @param policy policy + * @param referenceTime validation date + * @return subkey revocation signature + */ + @JvmStatic + fun pickCurrentSubkeyBindingRevocationSignature( + keyRing: PGPKeyRing, + subkey: PGPPublicKey, + policy: Policy, + referenceTime: Date + ): PGPSignature? { + val primaryKey = keyRing.publicKey + require(primaryKey.keyID != subkey.keyID) { + "Primary key cannot have subkey binding revocations." + } + return getSortedSignaturesOfType(subkey, SignatureType.SUBKEY_REVOCATION).lastOrNull { + return@lastOrNull try { + SignatureVerifier.verifySubkeyBindingRevocation( + it, primaryKey, subkey, policy, referenceTime) + true + } catch (e: SignatureValidationException) { + false + } + } + } + + /** + * Pick the at validationDate latest, valid subkey binding signature for the given subkey. + * This method might return null, if there is no subkey binding signature which is valid at + * validationDate. + * + * @param keyRing key ring + * @param subkey subkey + * @param policy policy + * @param referenceTime date of validation + * @return most recent valid subkey binding signature + */ + @JvmStatic + fun pickCurrentSubkeyBindingSignature( + keyRing: PGPKeyRing, + subkey: PGPPublicKey, + policy: Policy, + referenceTime: Date + ): PGPSignature? { + val primaryKey = keyRing.publicKey + require(primaryKey.keyID != subkey.keyID) { + "Primary key cannot have subkey binding signatures." + } + return getSortedSignaturesOfType(subkey, SignatureType.SUBKEY_BINDING).lastOrNull { + return@lastOrNull try { + SignatureVerifier.verifySubkeyBindingSignature( + it, primaryKey, subkey, policy, referenceTime) + true + } catch (e: SignatureValidationException) { + false + } + } + } + + /** + * Pick the at validationDate latest subkey binding signature for the given subkey. This + * method might return an expired signature. If a non-expired subkey binding signature + * exists, the latest non-expired yet already effective subkey binding signature for the + * given subkey will be returned. + * + * @param keyRing key ring + * @param subkey subkey + * @param policy policy + * @param referenceTime validationDate + * @return subkey binding signature + */ + @JvmStatic + fun pickLatestSubkeyBindingSignature( + keyRing: PGPKeyRing, + subkey: PGPPublicKey, + policy: Policy, + referenceTime: Date + ): PGPSignature? { + val primaryKey = keyRing.publicKey + require(primaryKey.keyID != subkey.keyID) { + "Primary key cannot have subkey binding signatures." + } + var latest: PGPSignature? = null + return getSortedSignaturesOfType(subkey, SignatureType.SUBKEY_BINDING).lastOrNull { + return@lastOrNull try { + SignatureValidator.signatureIsOfType(SignatureType.SUBKEY_BINDING).verify(it) + SignatureValidator.signatureStructureIsAcceptable(primaryKey, policy).verify(it) + SignatureValidator.signatureDoesNotPredateSignee(subkey).verify(it) + SignatureValidator.signatureIsAlreadyEffective(referenceTime).verify(it) + // if the currently latest signature is not yet expired, check if the next + // candidate is not yet expired + if (latest != null && !latest!!.isExpired(referenceTime)) { + SignatureValidator.signatureIsNotYetExpired(referenceTime).verify(it) + } + SignatureValidator.correctSubkeyBindingSignature(primaryKey, subkey).verify(it) + latest = it + true + } catch (e: SignatureValidationException) { + false + } + } + } + + @JvmStatic + private fun getSortedSignaturesOfType( + key: PGPPublicKey, + type: SignatureType + ): List = + key.getSignaturesOfType(type.code) + .asSequence() + .sortedWith(SignatureCreationDateComparator()) + .toList() + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureValidator.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureValidator.kt new file mode 100644 index 00000000..7cc384e1 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureValidator.kt @@ -0,0 +1,701 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.consumer + +import java.lang.Exception +import java.util.Date +import openpgp.formatUTC +import openpgp.openPgpKeyId +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPPublicKey +import org.bouncycastle.openpgp.PGPSignature +import org.bouncycastle.openpgp.PGPUserAttributeSubpacketVector +import org.pgpainless.algorithm.KeyFlag +import org.pgpainless.algorithm.SignatureSubpacket +import org.pgpainless.algorithm.SignatureType +import org.pgpainless.bouncycastle.extensions.fingerprint +import org.pgpainless.bouncycastle.extensions.isHardRevocation +import org.pgpainless.bouncycastle.extensions.isOfType +import org.pgpainless.bouncycastle.extensions.publicKeyAlgorithm +import org.pgpainless.bouncycastle.extensions.signatureExpirationDate +import org.pgpainless.bouncycastle.extensions.signatureHashAlgorithm +import org.pgpainless.exception.SignatureValidationException +import org.pgpainless.implementation.ImplementationFactory +import org.pgpainless.key.OpenPgpFingerprint +import org.pgpainless.policy.Policy +import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil +import org.pgpainless.util.NotationRegistry + +abstract class SignatureValidator { + + @Throws(SignatureValidationException::class) abstract fun verify(signature: PGPSignature) + + companion object { + + /** + * Check, whether there is the possibility that the given signature was created by the given + * key. [verify] throws a [SignatureValidationException] if we can say with certainty that + * the signature was not created by the given key (e.g. if the sig carries another issuer, + * issuer fingerprint packet). + * + * If there is no information found in the signature about who created it (no issuer, no + * fingerprint), [verify] will simply return since it is plausible that the given key + * created the sig. + * + * @param signingKey signing key + * @return validator that throws a [SignatureValidationException] if the signature was not + * possibly made by the given key. + */ + @JvmStatic + fun wasPossiblyMadeByKey(signingKey: PGPPublicKey): SignatureValidator { + return object : SignatureValidator() { + override fun verify(signature: PGPSignature) { + val signingKeyFingerprint = OpenPgpFingerprint.of(signingKey) + val issuer = SignatureSubpacketsUtil.getIssuerKeyIdAsLong(signature) + + if (issuer != null) { + if (issuer != signingKey.keyID) { + throw SignatureValidationException( + "Signature was not created by" + + " $signingKeyFingerprint (signature issuer: ${issuer.openPgpKeyId()})") + } + } + + if (signature.fingerprint != null && + signature.fingerprint != signingKeyFingerprint) { + throw SignatureValidationException( + "Signature was not created by" + + " $signingKeyFingerprint (signature fingerprint: ${signature.fingerprint})") + } + } + + // No issuer information found, so we cannot rule out that we did not create the sig + } + } + + /** + * Verify that a subkey binding signature - if the subkey is signing-capable - contains a + * valid primary key binding signature. + * + * @param primaryKey primary key + * @param subkey subkey + * @param policy policy + * @param referenceDate reference date for signature verification + * @return validator + */ + @JvmStatic + fun hasValidPrimaryKeyBindingSignatureIfRequired( + primaryKey: PGPPublicKey, + subkey: PGPPublicKey, + policy: Policy, + referenceTime: Date + ): SignatureValidator { + return object : SignatureValidator() { + override fun verify(signature: PGPSignature) { + if (!signature.publicKeyAlgorithm.isSigningCapable()) { + // subkey is not signing capable -> No need to process embedded signatures + return + } + + // Make sure we have key flags + SignatureSubpacketsUtil.getKeyFlags(signature)?.let { + if (!KeyFlag.hasKeyFlag(it.flags, KeyFlag.SIGN_DATA) && + !KeyFlag.hasKeyFlag(it.flags, KeyFlag.CERTIFY_OTHER)) { + return + } + } + ?: return + + try { + val embeddedSignatures = + SignatureSubpacketsUtil.getEmbeddedSignature(signature) + if (embeddedSignatures.isEmpty) { + throw SignatureValidationException( + "Missing primary key binding" + + " signature on signing capable subkey ${subkey.keyID.openPgpKeyId()}", + mapOf()) + } + + val rejectedEmbeddedSignatures = mutableMapOf() + if (!embeddedSignatures.any { embedded -> + if (embedded.isOfType(SignatureType.PRIMARYKEY_BINDING)) { + try { + signatureStructureIsAcceptable(subkey, policy).verify(embedded) + signatureIsEffective(referenceTime).verify(embedded) + correctPrimaryKeyBindingSignature(primaryKey, subkey) + .verify(embedded) + return@any true + } catch (e: SignatureValidationException) { + rejectedEmbeddedSignatures[embedded] = e + } + } + false + }) { + throw SignatureValidationException( + "Missing primary key binding signature on signing capable subkey ${subkey.keyID.openPgpKeyId()}", + rejectedEmbeddedSignatures) + } + } catch (e: PGPException) { + throw SignatureValidationException( + "Cannot process list of embedded signatures.", e) + } + } + } + } + + /** + * Verify that a signature has an acceptable structure. + * + * @param signingKey signing key + * @param policy policy + * @return validator + */ + @JvmStatic + fun signatureStructureIsAcceptable( + signingKey: PGPPublicKey, + policy: Policy + ): SignatureValidator { + return object : SignatureValidator() { + override fun verify(signature: PGPSignature) { + signatureIsNotMalformed(signingKey).verify(signature) + if (signature.version >= 4) { + signatureDoesNotHaveCriticalUnknownNotations(policy.notationRegistry) + .verify(signature) + signatureDoesNotHaveCriticalUnknownSubpackets().verify(signature) + } + signatureUsesAcceptableHashAlgorithm(policy).verify(signature) + signatureUsesAcceptablePublicKeyAlgorithm(policy, signingKey).verify(signature) + } + } + } + + /** + * Verify that a signature was made using an acceptable [PublicKeyAlgorithm]. + * + * @param policy policy + * @param signingKey signing key + * @return validator + */ + @JvmStatic + fun signatureUsesAcceptablePublicKeyAlgorithm( + policy: Policy, + signingKey: PGPPublicKey + ): SignatureValidator { + return object : SignatureValidator() { + override fun verify(signature: PGPSignature) { + if (signingKey.bitStrength == -1) { + throw SignatureValidationException( + "Cannot determine bit strength of signing key.") + } + if (!policy.publicKeyAlgorithmPolicy.isAcceptable( + signingKey.publicKeyAlgorithm, signingKey.bitStrength)) { + throw SignatureValidationException( + "Signature was made using unacceptable key. " + + "${signingKey.publicKeyAlgorithm} (${signingKey.bitStrength} bits) is " + + "not acceptable according to the public key algorithm policy.") + } + } + } + } + + @JvmStatic + fun signatureUsesAcceptableHashAlgorithm(policy: Policy): SignatureValidator { + return object : SignatureValidator() { + override fun verify(signature: PGPSignature) { + try { + val algorithmPolicy = getHashAlgorithmPolicyForSignature(signature, policy) + if (!algorithmPolicy.isAcceptable( + signature.signatureHashAlgorithm, signature.creationTime)) { + throw SignatureValidationException( + "Signature uses unacceptable" + + " hash algorithm ${signature.signatureHashAlgorithm}" + + " (Signature creation time: ${signature.creationTime.formatUTC()})") + } + } catch (e: NoSuchElementException) { + throw SignatureValidationException( + "Signature uses unknown hash" + " algorithm ${signature.hashAlgorithm}") + } + } + } + } + + /** + * Return the applicable [Policy.HashAlgorithmPolicy] for the given [PGPSignature]. + * Revocation signatures are being policed using a different policy than non-revocation + * signatures. + * + * @param signature signature + * @param policy revocation policy for revocation sigs, normal policy for non-rev sigs + * @return policy + */ + @JvmStatic + private fun getHashAlgorithmPolicyForSignature( + signature: PGPSignature, + policy: Policy + ): Policy.HashAlgorithmPolicy { + return when (SignatureType.fromCode(signature.signatureType)) { + null -> policy.certificationSignatureHashAlgorithmPolicy + SignatureType.CERTIFICATION_REVOCATION, + SignatureType.KEY_REVOCATION, + SignatureType.SUBKEY_REVOCATION -> policy.revocationSignatureHashAlgorithmPolicy + SignatureType.GENERIC_CERTIFICATION, + SignatureType.NO_CERTIFICATION, + SignatureType.CASUAL_CERTIFICATION, + SignatureType.POSITIVE_CERTIFICATION, + SignatureType.DIRECT_KEY, + SignatureType.SUBKEY_BINDING, + SignatureType.PRIMARYKEY_BINDING -> policy.certificationSignatureHashAlgorithmPolicy + else -> policy.dataSignatureHashAlgorithmPolicy + } + } + + /** + * Verify that a signature does not carry critical unknown notations. + * + * @param registry notation registry of known notations + * @return validator + */ + @JvmStatic + fun signatureDoesNotHaveCriticalUnknownNotations( + registry: NotationRegistry + ): SignatureValidator { + return object : SignatureValidator() { + override fun verify(signature: PGPSignature) { + SignatureSubpacketsUtil.getHashedNotationData(signature) + .filter { it.isCritical && !registry.isKnownNotation(it.notationName) } + .forEach { + throw SignatureValidationException( + "Signature contains unknown critical notation '${it.notationName}' in its hashed area.") + } + } + } + } + + /** + * Verify that a signature does not contain critical unknown subpackets. + * + * @return validator + */ + @JvmStatic + fun signatureDoesNotHaveCriticalUnknownSubpackets(): SignatureValidator { + return object : SignatureValidator() { + override fun verify(signature: PGPSignature) { + signature.hashedSubPackets.criticalTags.forEach { + try { + SignatureSubpacket.requireFromCode(it) + } catch (e: NoSuchElementException) { + throw SignatureValidationException( + "Signature contains unknown critical subpacket of type 0x${Integer.toHexString(it)}") + } + } + } + } + } + + /** + * Verify that a signature is effective at the given reference date. + * + * @param referenceTime reference date for signature verification + * @return validator + */ + @JvmStatic + @JvmOverloads + fun signatureIsEffective(referenceTime: Date = Date()): SignatureValidator { + return object : SignatureValidator() { + override fun verify(signature: PGPSignature) { + signatureIsAlreadyEffective(referenceTime).verify(signature) + signatureIsNotYetExpired(referenceTime).verify(signature) + } + } + } + + /** + * Verify that a signature was created prior to the given reference date. + * + * @param referenceTime reference date for signature verification + * @return validator + */ + @JvmStatic + fun signatureIsAlreadyEffective(referenceTime: Date): SignatureValidator { + return object : SignatureValidator() { + override fun verify(signature: PGPSignature) { + if (signature.isHardRevocation) { + return + } + if (signature.creationTime > referenceTime) { + throw SignatureValidationException( + "Signature was created at ${signature.creationTime.formatUTC()} and" + + " is therefore not yet valid at ${referenceTime.formatUTC()}") + } + } + } + } + + /** + * Verify that a signature is not yet expired. + * + * @param referenceTime reference date for signature verification + * @return validator + */ + @JvmStatic + fun signatureIsNotYetExpired(referenceTime: Date): SignatureValidator { + return object : SignatureValidator() { + override fun verify(signature: PGPSignature) { + if (signature.isHardRevocation) { + return + } + val expirationDate = signature.signatureExpirationDate + if (expirationDate != null && expirationDate < referenceTime) { + throw SignatureValidationException( + "Signature is already expired " + + "(expiration: ${expirationDate.formatUTC()}," + + " validation: ${referenceTime.formatUTC()})") + } + } + } + } + + /** + * Verify that a signature is not malformed. A signature is malformed if it has no hashed + * creation time subpacket, it predates the creation time of the signing key, or it predates + * the creation date of the signing key binding signature. + * + * @param signingKey signing key + * @return validator + */ + @JvmStatic + fun signatureIsNotMalformed(signingKey: PGPPublicKey): SignatureValidator { + return object : SignatureValidator() { + override fun verify(signature: PGPSignature) { + if (signature.version >= 4) { + signatureHasHashedCreationTime().verify(signature) + } + signatureDoesNotPredateSigningKey(signingKey).verify(signature) + if (!signature.isOfType(SignatureType.PRIMARYKEY_BINDING)) { + signatureDoesNotPredateSigningKeyBindingDate(signingKey).verify(signature) + } + } + } + } + + @JvmStatic + fun signatureDoesNotPredateSignee(signee: PGPPublicKey): SignatureValidator { + return signatureDoesNotPredateKeyCreation(signee) + } + + /** + * Verify that a signature does not predate the creation time of the signing key. + * + * @param key signing key + * @return validator + */ + @JvmStatic + fun signatureDoesNotPredateSigningKey(signingKey: PGPPublicKey): SignatureValidator { + return signatureDoesNotPredateKeyCreation(signingKey) + } + + /** + * Verify that a signature does not predate the creation time of the given key. + * + * @param key key + * @return validator + */ + @JvmStatic + fun signatureDoesNotPredateKeyCreation(key: PGPPublicKey): SignatureValidator { + return object : SignatureValidator() { + override fun verify(signature: PGPSignature) { + if (key.creationTime > signature.creationTime) { + throw SignatureValidationException( + "Signature predates key" + + " (key creation: ${key.creationTime.formatUTC()}," + + " signature creation: ${signature.creationTime.formatUTC()})") + } + } + } + } + + /** + * Verify that a signature has a hashed creation time subpacket. + * + * @return validator + */ + @JvmStatic + fun signatureHasHashedCreationTime(): SignatureValidator { + return object : SignatureValidator() { + override fun verify(signature: PGPSignature) { + if (SignatureSubpacketsUtil.getSignatureCreationTime(signature) == null) { + throw SignatureValidationException( + "Malformed signature." + + "Signature has no signature creation time subpacket in its hashed area.") + } + } + } + } + + /** + * Verify that a signature does not predate the binding date of the signing key. + * + * @param signingKey signing key + * @return validator + */ + @JvmStatic + fun signatureDoesNotPredateSigningKeyBindingDate( + signingKey: PGPPublicKey + ): SignatureValidator { + return object : SignatureValidator() { + override fun verify(signature: PGPSignature) { + if (signingKey.isMasterKey) { + return + } + if (signingKey + .getSignaturesOfType(SignatureType.SUBKEY_BINDING.code) + .asSequence() + .map { + if (signature.creationTime < it.creationTime) { + throw SignatureValidationException( + "Signature was created " + + "before the signing key was bound to the certificate.") + } + } + .none()) { + throw SignatureValidationException( + "Signing subkey does not have a subkey binding signature.") + } + } + } + } + + /** + * Verify that a subkey binding signature is correct. + * + * @param primaryKey primary key + * @param subkey subkey + * @return validator + */ + @JvmStatic + fun correctSubkeyBindingSignature( + primaryKey: PGPPublicKey, + subkey: PGPPublicKey + ): SignatureValidator { + return object : SignatureValidator() { + override fun verify(signature: PGPSignature) { + if (primaryKey.keyID == subkey.keyID) { + throw SignatureValidationException("Primary key cannot be its own subkey.") + } + try { + signature.init( + ImplementationFactory.getInstance().pgpContentVerifierBuilderProvider, + primaryKey) + if (!signature.verifyCertification(primaryKey, subkey)) { + throw SignatureValidationException("Signature is not correct.") + } + } catch (e: PGPException) { + throw SignatureValidationException( + "Cannot verify subkey binding signature correctness", e) + } catch (e: ClassCastException) { + throw SignatureValidationException( + "Cannot verify subkey binding signature correctness", e) + } + } + } + } + + /** + * Verify that a primary key binding signature is correct. + * + * @param primaryKey primary key + * @param subkey subkey + * @return validator + */ + @JvmStatic + fun correctPrimaryKeyBindingSignature( + primaryKey: PGPPublicKey, + subkey: PGPPublicKey + ): SignatureValidator { + return object : SignatureValidator() { + override fun verify(signature: PGPSignature) { + if (primaryKey.keyID == subkey.keyID) { + throw SignatureValidationException("Primary key cannot be its own subkey.") + } + try { + signature.init( + ImplementationFactory.getInstance().pgpContentVerifierBuilderProvider, + subkey) + if (!signature.verifyCertification(primaryKey, subkey)) { + throw SignatureValidationException( + "Primary Key Binding Signature is not correct.") + } + } catch (e: PGPException) { + throw SignatureValidationException( + "Cannot verify primary key binding signature correctness", e) + } catch (e: ClassCastException) { + throw SignatureValidationException( + "Cannot verify primary key binding signature correctness", e) + } + } + } + } + + /** + * Verify that a direct-key signature is correct. + * + * @param signingKey signing key + * @param signedKey signed key + * @return validator + */ + @JvmStatic + fun correctSignatureOverKey( + signingKey: PGPPublicKey, + signedKey: PGPPublicKey + ): SignatureValidator { + return object : SignatureValidator() { + override fun verify(signature: PGPSignature) { + try { + signature.init( + ImplementationFactory.getInstance().pgpContentVerifierBuilderProvider, + signingKey) + val valid = + if (signingKey.keyID == signedKey.keyID || + signature.isOfType(SignatureType.DIRECT_KEY)) { + signature.verifyCertification(signedKey) + } else { + signature.verifyCertification(signingKey, signedKey) + } + if (!valid) { + throw SignatureValidationException("Signature is not correct.") + } + } catch (e: PGPException) { + throw SignatureValidationException( + "Cannot verify direct-key signature correctness", e) + } catch (e: ClassCastException) { + throw SignatureValidationException( + "Cannot verify direct-key signature correctness", e) + } + } + } + } + + @JvmStatic + fun signatureIsCertification(): SignatureValidator { + return signatureIsOfType( + SignatureType.POSITIVE_CERTIFICATION, + SignatureType.CASUAL_CERTIFICATION, + SignatureType.GENERIC_CERTIFICATION, + SignatureType.NO_CERTIFICATION) + } + + /** + * Verify that a signature type equals one of the given [SignatureType]. + * + * @param signatureType one or more signature types + * @return validator + */ + @JvmStatic + fun signatureIsOfType(vararg signatureType: SignatureType): SignatureValidator { + return object : SignatureValidator() { + override fun verify(signature: PGPSignature) { + if (signatureType.none { signature.isOfType(it) }) { + throw SignatureValidationException( + "Signature is of type" + + " ${SignatureType.fromCode(signature.signatureType) ?: + ("0x" + signature.signatureType.toString(16))}, " + + "while only ${signatureType.contentToString()} are allowed here.") + } + } + } + } + + /** + * Verify that a signature over a user-id is correct. + * + * @param userId user-id + * @param certifiedKey key carrying the user-id + * @param certifyingKey key that created the signature. + * @return validator + */ + @JvmStatic + fun correctSignatureOverUserId( + userId: CharSequence, + certifiedKey: PGPPublicKey, + certifyingKey: PGPPublicKey + ): SignatureValidator { + return object : SignatureValidator() { + override fun verify(signature: PGPSignature) { + try { + signature.init( + ImplementationFactory.getInstance().pgpContentVerifierBuilderProvider, + certifyingKey) + if (!signature.verifyCertification(userId.toString(), certifiedKey)) { + throw SignatureValidationException( + "Signature over user-id '$userId' is not valid.") + } + } catch (e: PGPException) { + throw SignatureValidationException( + "Cannot verify signature over user-id '$userId'.", e) + } catch (e: ClassCastException) { + throw SignatureValidationException( + "Cannot verify signature over user-id '$userId'.", e) + } + } + } + } + + /** + * Verify that a signature over a user-attribute packet is correct. + * + * @param userAttributes user attributes + * @param certifiedKey key carrying the user-attributes + * @param certifyingKey key that created the certification signature + * @return validator + */ + @JvmStatic + fun correctSignatureOverUserAttributes( + userAttributes: PGPUserAttributeSubpacketVector, + certifiedKey: PGPPublicKey, + certifyingKey: PGPPublicKey + ): SignatureValidator { + return object : SignatureValidator() { + override fun verify(signature: PGPSignature) { + try { + signature.init( + ImplementationFactory.getInstance().pgpContentVerifierBuilderProvider, + certifyingKey) + if (!signature.verifyCertification(userAttributes, certifiedKey)) { + throw SignatureValidationException( + "Signature over user-attributes is not correct.") + } + } catch (e: PGPException) { + throw SignatureValidationException( + "Cannot verify signature over user-attribute vector.", e) + } catch (e: ClassCastException) { + throw SignatureValidationException( + "Cannot verify signature over user-attribute vector.", e) + } + } + } + } + + @JvmStatic + fun signatureWasCreatedInBounds(notBefore: Date?, notAfter: Date?): SignatureValidator { + return object : SignatureValidator() { + override fun verify(signature: PGPSignature) { + val timestamp = signature.creationTime + if (notBefore != null && timestamp < notBefore) { + throw SignatureValidationException( + "Signature was made before the earliest allowed signature creation time." + + " Created: ${timestamp.formatUTC()}," + + " earliest allowed: ${notBefore.formatUTC()}") + } + if (notAfter != null && timestamp > notAfter) { + throw SignatureValidationException( + "Signature was made after the latest allowed signature creation time." + + " Created: ${timestamp.formatUTC()}," + + " latest allowed: ${notAfter.formatUTC()}") + } + } + } + } + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureValidityComparator.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureValidityComparator.kt new file mode 100644 index 00000000..1153b875 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureValidityComparator.kt @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.consumer + +import org.bouncycastle.openpgp.PGPSignature +import org.pgpainless.bouncycastle.extensions.isHardRevocation + +/** + * Comparator which sorts signatures based on an ordering and on revocation hardness. + * + * If a list of signatures gets ordered using this comparator, hard revocations will always come + * first. Further, signatures are ordered by date according to the + * [SignatureCreationDateComparator.Order]. + */ +class SignatureValidityComparator( + order: SignatureCreationDateComparator.Order = SignatureCreationDateComparator.Order.OLD_TO_NEW +) : Comparator { + + private val creationDateComparator: SignatureCreationDateComparator = + SignatureCreationDateComparator(order) + + override fun compare(one: PGPSignature, two: PGPSignature): Int { + return if (one.isHardRevocation == two.isHardRevocation) { + // Both have the same hardness, so compare creation time + creationDateComparator.compare(one, two) + } + // else favor the "harder" signature + else if (one.isHardRevocation) -1 else 1 + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureVerifier.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureVerifier.kt new file mode 100644 index 00000000..d51b2379 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureVerifier.kt @@ -0,0 +1,597 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.consumer + +import java.io.IOException +import java.io.InputStream +import java.util.* +import openpgp.openPgpKeyId +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPPublicKey +import org.bouncycastle.openpgp.PGPSignature +import org.bouncycastle.openpgp.PGPUserAttributeSubpacketVector +import org.pgpainless.algorithm.SignatureType +import org.pgpainless.exception.SignatureValidationException +import org.pgpainless.implementation.ImplementationFactory.Companion.getInstance +import org.pgpainless.policy.Policy +import org.pgpainless.signature.consumer.SignatureValidator.Companion.correctSignatureOverKey +import org.pgpainless.signature.consumer.SignatureValidator.Companion.correctSignatureOverUserAttributes +import org.pgpainless.signature.consumer.SignatureValidator.Companion.correctSignatureOverUserId +import org.pgpainless.signature.consumer.SignatureValidator.Companion.correctSubkeyBindingSignature +import org.pgpainless.signature.consumer.SignatureValidator.Companion.hasValidPrimaryKeyBindingSignatureIfRequired +import org.pgpainless.signature.consumer.SignatureValidator.Companion.signatureDoesNotPredateSignee +import org.pgpainless.signature.consumer.SignatureValidator.Companion.signatureIsCertification +import org.pgpainless.signature.consumer.SignatureValidator.Companion.signatureIsEffective +import org.pgpainless.signature.consumer.SignatureValidator.Companion.signatureIsOfType +import org.pgpainless.signature.consumer.SignatureValidator.Companion.signatureStructureIsAcceptable +import org.pgpainless.signature.consumer.SignatureValidator.Companion.wasPossiblyMadeByKey + +/** + * Collection of static methods for signature verification. Signature verification entails + * validation of certain criteria (see [SignatureValidator]), as well as cryptographic verification + * of signature correctness. + */ +class SignatureVerifier { + + companion object { + + /** + * Verify a signature (certification or revocation) over a user-id. + * + * @param userId user-id + * @param signature certification signature + * @param signingKey key that created the certification + * @param keyWithUserId key carrying the user-id + * @param policy policy + * @param referenceTime reference date for signature verification + * @return true if signature verification is successful + * @throws SignatureValidationException if signature verification fails for some reason + */ + @JvmStatic + @Throws(SignatureValidationException::class) + fun verifySignatureOverUserId( + userId: CharSequence, + signature: PGPSignature, + signingKey: PGPPublicKey, + keyWithUserId: PGPPublicKey, + policy: Policy, + referenceTime: Date + ): Boolean { + val type = SignatureType.fromCode(signature.signatureType) + return when (type) { + SignatureType.GENERIC_CERTIFICATION, + SignatureType.NO_CERTIFICATION, + SignatureType.CASUAL_CERTIFICATION, + SignatureType.POSITIVE_CERTIFICATION, + null -> + verifyUserIdCertification( + userId, signature, signingKey, keyWithUserId, policy, referenceTime) + SignatureType.CERTIFICATION_REVOCATION -> + verifyUserIdRevocation( + userId, signature, signingKey, keyWithUserId, policy, referenceTime) + else -> + throw SignatureValidationException( + "Signature is not a valid user-id certification/revocation signature: $type") + } + } + + /** + * Verify a certification self-signature over a user-id. + * + * @param userId user-id + * @param signature certification signature + * @param primaryKey primary key + * @param policy policy + * @param referenceTime reference date for signature verification + * @return true if the self-signature is verified successfully + * @throws SignatureValidationException if signature verification fails for some reason + */ + @JvmStatic + @Throws(SignatureValidationException::class) + fun verifyUserIdCertification( + userId: CharSequence, + signature: PGPSignature, + primaryKey: PGPPublicKey, + policy: Policy, + referenceTime: Date + ): Boolean { + return verifyUserIdCertification( + userId, signature, primaryKey, primaryKey, policy, referenceTime) + } + + /** + * Verify a user-id certification. + * + * @param userId user-id + * @param signature certification signature + * @param signingKey key that created the certification + * @param keyWithUserId primary key that carries the user-id + * @param policy policy + * @param referenceTime reference date for signature verification + * @return true if signature verification is successful + * @throws SignatureValidationException if signature verification fails for some reason + */ + @JvmStatic + @Throws(SignatureValidationException::class) + fun verifyUserIdCertification( + userId: CharSequence, + signature: PGPSignature, + signingKey: PGPPublicKey, + keyWithUserId: PGPPublicKey, + policy: Policy, + referenceTime: Date + ): Boolean { + wasPossiblyMadeByKey(signingKey).verify(signature) + signatureIsCertification().verify(signature) + signatureStructureIsAcceptable(signingKey, policy).verify(signature) + signatureIsEffective(referenceTime).verify(signature) + correctSignatureOverUserId(userId, keyWithUserId, signingKey).verify(signature) + + return true + } + + /** + * Verify a user-id revocation self-signature. + * + * @param userId user-id + * @param signature user-id revocation signature + * @param primaryKey primary key + * @param policy policy + * @param referenceTime reference date for signature verification + * @return true if the user-id revocation signature is successfully verified + * @throws SignatureValidationException if signature verification fails for some reason + */ + @JvmStatic + @Throws(SignatureValidationException::class) + fun verifyUserIdRevocation( + userId: CharSequence, + signature: PGPSignature, + primaryKey: PGPPublicKey, + policy: Policy, + referenceTime: Date + ): Boolean { + return verifyUserIdRevocation( + userId, signature, primaryKey, primaryKey, policy, referenceTime) + } + + /** + * Verify a user-id revocation signature. + * + * @param userId user-id + * @param signature revocation signature + * @param signingKey key that created the revocation signature + * @param keyWithUserId primary key carrying the user-id + * @param policy policy + * @param referenceTime reference date for signature verification + * @return true if the user-id revocation signature is successfully verified + * @throws SignatureValidationException if signature verification fails for some reason + */ + @JvmStatic + @Throws(SignatureValidationException::class) + fun verifyUserIdRevocation( + userId: CharSequence, + signature: PGPSignature, + signingKey: PGPPublicKey, + keyWithUserId: PGPPublicKey, + policy: Policy, + referenceTime: Date + ): Boolean { + wasPossiblyMadeByKey(signingKey).verify(signature) + signatureIsOfType(SignatureType.CERTIFICATION_REVOCATION).verify(signature) + signatureStructureIsAcceptable(signingKey, policy).verify(signature) + signatureIsEffective(referenceTime).verify(signature) + correctSignatureOverUserId(userId, keyWithUserId, signingKey).verify(signature) + + return true + } + + /** + * Verify a certification self-signature over a user-attributes packet. + * + * @param userAttributes user attributes + * @param signature certification self-signature + * @param primaryKey primary key that carries the user-attributes + * @param policy policy + * @param referenceTime reference date for signature verification + * @return true if the signature can be verified successfully + * @throws SignatureValidationException if signature verification fails for some reason + */ + @JvmStatic + @Throws(SignatureValidationException::class) + fun verifyUserAttributesCertification( + userAttributes: PGPUserAttributeSubpacketVector, + signature: PGPSignature, + primaryKey: PGPPublicKey, + policy: Policy, + referenceTime: Date + ): Boolean { + return verifyUserAttributesCertification( + userAttributes, signature, primaryKey, primaryKey, policy, referenceTime) + } + + /** + * Verify a certification signature over a user-attributes packet. + * + * @param userAttributes user attributes + * @param signature certification signature + * @param signingKey key that created the user-attributes certification + * @param keyWithAttributes key that carries the user-attributes certification + * @param policy policy + * @param referenceTime reference date for signature verification + * @return true if the signature can be verified successfully + * @throws SignatureValidationException if signature verification fails for some reason + */ + @JvmStatic + @Throws(SignatureValidationException::class) + fun verifyUserAttributesCertification( + userAttributes: PGPUserAttributeSubpacketVector, + signature: PGPSignature, + signingKey: PGPPublicKey, + keyWithAttributes: PGPPublicKey, + policy: Policy, + referenceTime: Date + ): Boolean { + wasPossiblyMadeByKey(signingKey).verify(signature) + signatureIsCertification().verify(signature) + signatureStructureIsAcceptable(signingKey, policy).verify(signature) + signatureIsEffective(referenceTime).verify(signature) + correctSignatureOverUserAttributes(userAttributes, keyWithAttributes, signingKey) + .verify(signature) + + return true + } + + /** + * Verify a user-attributes revocation self-signature. + * + * @param userAttributes user-attributes + * @param signature user-attributes revocation signature + * @param primaryKey primary key that carries the user-attributes + * @param policy policy + * @param referenceTime reference date for signature verification + * @return true if the revocation signature can be verified successfully + * @throws SignatureValidationException if signature verification fails for some reason + */ + @JvmStatic + @Throws(SignatureValidationException::class) + fun verifyUserAttributesRevocation( + userAttributes: PGPUserAttributeSubpacketVector, + signature: PGPSignature, + primaryKey: PGPPublicKey, + policy: Policy, + referenceTime: Date + ): Boolean { + return verifyUserAttributesRevocation( + userAttributes, signature, primaryKey, primaryKey, policy, referenceTime) + } + + /** + * Verify a user-attributes revocation signature. + * + * @param userAttributes user-attributes + * @param signature revocation signature + * @param signingKey revocation key + * @param keyWithAttributes key that carries the user-attributes + * @param policy policy + * @param referenceTime reference date for signature verification + * @return true if the revocation signature can be verified successfully + * @throws SignatureValidationException if signature verification fails for some reason + */ + @JvmStatic + @Throws(SignatureValidationException::class) + fun verifyUserAttributesRevocation( + userAttributes: PGPUserAttributeSubpacketVector, + signature: PGPSignature, + signingKey: PGPPublicKey, + keyWithAttributes: PGPPublicKey, + policy: Policy, + referenceTime: Date + ): Boolean { + wasPossiblyMadeByKey(signingKey).verify(signature) + signatureIsOfType(SignatureType.CERTIFICATION_REVOCATION).verify(signature) + signatureStructureIsAcceptable(signingKey, policy).verify(signature) + signatureIsEffective(referenceTime).verify(signature) + correctSignatureOverUserAttributes(userAttributes, keyWithAttributes, signingKey) + .verify(signature) + + return true + } + + /** + * Verify a subkey binding signature. + * + * @param signature binding signature + * @param primaryKey primary key + * @param subkey subkey + * @param policy policy + * @param referenceTime reference date for signature verification + * @return true if the binding signature can be verified successfully + * @throws SignatureValidationException if signature verification fails for some reason + */ + @JvmStatic + @Throws(SignatureValidationException::class) + fun verifySubkeyBindingSignature( + signature: PGPSignature, + primaryKey: PGPPublicKey, + subkey: PGPPublicKey, + policy: Policy, + referenceTime: Date + ): Boolean { + signatureIsOfType(SignatureType.SUBKEY_BINDING).verify(signature) + signatureStructureIsAcceptable(primaryKey, policy).verify(signature) + signatureDoesNotPredateSignee(subkey).verify(signature) + signatureIsEffective(referenceTime).verify(signature) + hasValidPrimaryKeyBindingSignatureIfRequired(primaryKey, subkey, policy, referenceTime) + .verify(signature) + correctSubkeyBindingSignature(primaryKey, subkey).verify(signature) + + return true + } + + /** + * Verify a subkey revocation signature. + * + * @param signature subkey revocation signature + * @param primaryKey primary key + * @param subkey subkey + * @param policy policy + * @param referenceTime reference date for signature verification + * @return true if the subkey revocation signature can be verified successfully + * @throws SignatureValidationException if signature verification fails for some reason + */ + @JvmStatic + @Throws(SignatureValidationException::class) + fun verifySubkeyBindingRevocation( + signature: PGPSignature, + primaryKey: PGPPublicKey, + subkey: PGPPublicKey, + policy: Policy, + referenceTime: Date + ): Boolean { + signatureIsOfType(SignatureType.SUBKEY_REVOCATION).verify(signature) + signatureStructureIsAcceptable(primaryKey, policy).verify(signature) + signatureDoesNotPredateSignee(subkey).verify(signature) + signatureIsEffective(referenceTime).verify(signature) + correctSignatureOverKey(primaryKey, subkey).verify(signature) + + return true + } + + /** + * Verify a direct-key self-signature. + * + * @param signature signature + * @param primaryKey primary key + * @param policy policy + * @param referenceTime reference date for signature verification + * @return true if the signature can be verified successfully + * @throws SignatureValidationException if signature verification fails for some reason + */ + @JvmStatic + @Throws(SignatureValidationException::class) + fun verifyDirectKeySignature( + signature: PGPSignature, + primaryKey: PGPPublicKey, + policy: Policy, + referenceTime: Date + ): Boolean { + return verifyDirectKeySignature( + signature, primaryKey, primaryKey, policy, referenceTime) + } + + /** + * Verify a direct-key signature. + * + * @param signature signature + * @param signingKey signing key + * @param signedKey signed key + * @param policy policy + * @param referenceTime reference date for signature verification + * @return true if signature verification is successful + * @throws SignatureValidationException if signature verification fails for some reason + */ + @JvmStatic + @Throws(SignatureValidationException::class) + fun verifyDirectKeySignature( + signature: PGPSignature, + signingKey: PGPPublicKey, + signedKey: PGPPublicKey, + policy: Policy, + referenceTime: Date + ): Boolean { + signatureIsOfType(SignatureType.DIRECT_KEY).verify(signature) + signatureStructureIsAcceptable(signingKey, policy).verify(signature) + signatureDoesNotPredateSignee(signedKey).verify(signature) + signatureIsEffective(referenceTime).verify(signature) + correctSignatureOverKey(signingKey, signedKey).verify(signature) + + return true + } + + /** + * Verify a key revocation signature. + * + * @param signature signature + * @param primaryKey primary key + * @param policy policy + * @param referenceTime reference date for signature verification + * @return true if signature verification is successful + * @throws SignatureValidationException if signature verification fails for some reason + */ + @JvmStatic + @Throws(SignatureValidationException::class) + fun verifyKeyRevocationSignature( + signature: PGPSignature, + primaryKey: PGPPublicKey, + policy: Policy, + referenceTime: Date + ): Boolean { + signatureIsOfType(SignatureType.KEY_REVOCATION).verify(signature) + signatureStructureIsAcceptable(primaryKey, policy).verify(signature) + signatureIsEffective(referenceTime).verify(signature) + correctSignatureOverKey(primaryKey, primaryKey).verify(signature) + + return true + } + + /** + * Initialize a signature and verify it afterwards by updating it with the signed data. + * + * @param signature OpenPGP signature + * @param signedData input stream containing the signed data + * @param signingKey the key that created the signature + * @param policy policy + * @param referenceTime reference date of signature verification + * @return true if the signature is successfully verified + * @throws SignatureValidationException if the signature verification fails for some reason + */ + @JvmStatic + @Throws(SignatureValidationException::class) + fun verifyUninitializedSignature( + signature: PGPSignature, + signedData: InputStream, + signingKey: PGPPublicKey, + policy: Policy, + referenceTime: Date + ): Boolean { + initializeSignatureAndUpdateWithSignedData(signature, signedData, signingKey) + return verifyInitializedSignature(signature, signingKey, policy, referenceTime) + } + + /** + * Initialize a signature and then update it with the signed data from the given + * [InputStream]. + * + * @param signature OpenPGP signature + * @param signedData input stream containing signed data + * @param signingKey key that created the signature + * @throws SignatureValidationException in case the signature cannot be verified for some + * reason + */ + @JvmStatic + @Throws(SignatureValidationException::class) + fun initializeSignatureAndUpdateWithSignedData( + signature: PGPSignature, + signedData: InputStream, + signingKey: PGPPublicKey + ) { + try { + signature.init( + getInstance().pgpContentVerifierBuilderProvider, + signingKey, + ) + var read: Int + val buf = ByteArray(8192) + var lastByte: Byte = -1 + while (signedData.read(buf).also { read = it } != -1) { + // If we previously omitted a newline, but the stream is not yet empty, add it + // now + if (lastByte == '\n'.code.toByte()) { + signature.update(lastByte) + } + lastByte = buf[read - 1] + if (lastByte == '\n'.code.toByte()) { + // if last byte in buffer is newline, omit it for now + signature.update(buf, 0, read - 1) + } else { + // otherwise, write buffer as usual + signature.update(buf, 0, read) + } + } + } catch (e: PGPException) { + throw SignatureValidationException("Cannot init signature.", e) + } catch (e: IOException) { + throw SignatureValidationException("Cannot update signature.", e) + } + } + + /** + * Verify an initialized signature. An initialized signature was already updated with the + * signed data. + * + * @param signature OpenPGP signature + * @param signingKey key that created the signature + * @param policy policy + * @param referenceTime reference date for signature verification + * @return true if signature is verified successfully + * @throws SignatureValidationException if signature verification fails for some reason + */ + @JvmStatic + @Throws(SignatureValidationException::class) + fun verifyInitializedSignature( + signature: PGPSignature, + signingKey: PGPPublicKey, + policy: Policy, + referenceTime: Date + ): Boolean { + wasPossiblyMadeByKey(signingKey).verify(signature) + signatureStructureIsAcceptable(signingKey, policy).verify(signature) + signatureIsEffective(referenceTime).verify(signature) + + return try { + if (!signature.verify()) { + throw SignatureValidationException("Signature is not correct.") + } + true + } catch (e: PGPException) { + throw SignatureValidationException("Could not verify signature correctness.", e) + } + } + + @JvmStatic + @Throws(SignatureValidationException::class) + fun verifyOnePassSignature( + signature: PGPSignature, + signingKey: PGPPublicKey, + onePassSignature: OnePassSignatureCheck, + policy: Policy + ): Boolean { + try { + wasPossiblyMadeByKey(signingKey).verify(signature) + signatureStructureIsAcceptable(signingKey, policy).verify(signature) + signatureIsEffective().verify(signature) + } catch (e: SignatureValidationException) { + throw SignatureValidationException("Signature is not valid: ${e.message}", e) + } + + try { + checkNotNull(onePassSignature.signature) { "No comparison signature provided." } + if (!onePassSignature.onePassSignature.verify(signature)) { + throw SignatureValidationException( + "Bad signature of key ${signingKey.keyID.openPgpKeyId()}") + } + } catch (e: PGPException) { + throw SignatureValidationException( + "Could not verify correctness of One-Pass-Signature: ${e.message}", e) + } + + return true + } + + /** + * Verify a signature (certification or revocation) over a user-id. + * + * @param userId user-id + * @param signature self-signature + * @param primaryKey primary key that created the signature + * @param policy policy + * @param referenceTime reference date for signature verification + * @return true if the signature is successfully verified + * @throws SignatureValidationException if signature verification fails for some reason + */ + @JvmStatic + @Throws(SignatureValidationException::class) + fun verifySignatureOverUserId( + userId: CharSequence, + signature: PGPSignature, + primaryKey: PGPPublicKey, + policy: Policy, + referenceTime: Date + ): Boolean { + return verifySignatureOverUserId( + userId, signature, primaryKey, primaryKey, policy, referenceTime) + } + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/BaseSignatureSubpackets.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/BaseSignatureSubpackets.kt new file mode 100644 index 00000000..b9d7fb3f --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/BaseSignatureSubpackets.kt @@ -0,0 +1,207 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.subpackets + +import java.io.IOException +import java.net.URL +import java.util.* +import org.bouncycastle.bcpg.sig.* +import org.bouncycastle.openpgp.PGPPublicKey +import org.bouncycastle.openpgp.PGPSignature +import org.pgpainless.algorithm.HashAlgorithm +import org.pgpainless.algorithm.PublicKeyAlgorithm + +interface BaseSignatureSubpackets { + + interface Callback : SignatureSubpacketCallback + + /** + * Add both an [IssuerKeyID] and [IssuerFingerprint] subpacket pointing to the given key. + * + * @param key key + * @return this + * @deprecated this method MUST NOT be used for OpenPGP v6, since v6 signatures MUST NOT contain + * any [IssuerKeyID] packets. + */ + fun setIssuerFingerprintAndKeyId(key: PGPPublicKey): BaseSignatureSubpackets + + fun setIssuerKeyId(keyId: Long): BaseSignatureSubpackets + + fun setIssuerKeyId(isCritical: Boolean, keyId: Long): BaseSignatureSubpackets + + fun setIssuerKeyId(issuerKeyID: IssuerKeyID?): BaseSignatureSubpackets + + fun setIssuerFingerprint(isCritical: Boolean, issuer: PGPPublicKey): BaseSignatureSubpackets + + fun setIssuerFingerprint(issuer: PGPPublicKey): BaseSignatureSubpackets + + fun setIssuerFingerprint(fingerprint: IssuerFingerprint?): BaseSignatureSubpackets + + fun setSignatureCreationTime(creationTime: Date): BaseSignatureSubpackets + + fun setSignatureCreationTime(isCritical: Boolean, creationTime: Date): BaseSignatureSubpackets + + fun setSignatureCreationTime(creationTime: SignatureCreationTime?): BaseSignatureSubpackets + + fun setSignatureExpirationTime( + creationTime: Date, + expirationTime: Date? + ): BaseSignatureSubpackets + + fun setSignatureExpirationTime( + isCritical: Boolean, + creationTime: Date, + expirationTime: Date? + ): BaseSignatureSubpackets + + fun setSignatureExpirationTime(isCritical: Boolean, seconds: Long): BaseSignatureSubpackets + + fun setSignatureExpirationTime( + expirationTime: SignatureExpirationTime? + ): BaseSignatureSubpackets + + fun setSignerUserId(userId: CharSequence): BaseSignatureSubpackets + + fun setSignerUserId(isCritical: Boolean, userId: CharSequence): BaseSignatureSubpackets + + fun setSignerUserId(signerUserID: SignerUserID?): BaseSignatureSubpackets + + fun addNotationData( + isCritical: Boolean, + notationName: String, + notationValue: String + ): BaseSignatureSubpackets + + fun addNotationData( + isCritical: Boolean, + isHumanReadable: Boolean, + notationName: String, + notationValue: String + ): BaseSignatureSubpackets + + fun addNotationData(notationData: NotationData): BaseSignatureSubpackets + + fun clearNotationData(): BaseSignatureSubpackets + + fun addIntendedRecipientFingerprint(recipientKey: PGPPublicKey): BaseSignatureSubpackets + + fun addIntendedRecipientFingerprint( + isCritical: Boolean, + recipientKey: PGPPublicKey + ): BaseSignatureSubpackets + + fun addIntendedRecipientFingerprint( + intendedRecipient: IntendedRecipientFingerprint + ): BaseSignatureSubpackets + + fun clearIntendedRecipientFingerprints(): BaseSignatureSubpackets + + fun setExportable(): BaseSignatureSubpackets + + fun setExportable(isExportable: Boolean): BaseSignatureSubpackets + + fun setExportable(isCritical: Boolean, isExportable: Boolean): BaseSignatureSubpackets + + fun setExportable(exportable: Exportable?): BaseSignatureSubpackets + + fun setPolicyUrl(policyUrl: URL): BaseSignatureSubpackets + + fun setPolicyUrl(isCritical: Boolean, policyUrl: URL): BaseSignatureSubpackets + + fun setPolicyUrl(policyUrl: PolicyURI?): BaseSignatureSubpackets + + fun setRegularExpression(regex: CharSequence): BaseSignatureSubpackets + + fun setRegularExpression(isCritical: Boolean, regex: CharSequence): BaseSignatureSubpackets + + fun setRegularExpression(regex: RegularExpression?): BaseSignatureSubpackets + + fun setRevocable(): BaseSignatureSubpackets + + fun setRevocable(isRevocable: Boolean): BaseSignatureSubpackets + + fun setRevocable(isCritical: Boolean, isRevocable: Boolean): BaseSignatureSubpackets + + fun setRevocable(revocable: Revocable?): BaseSignatureSubpackets + + fun setSignatureTarget( + keyAlgorithm: PublicKeyAlgorithm, + hashAlgorithm: HashAlgorithm, + hashData: ByteArray + ): BaseSignatureSubpackets + + fun setSignatureTarget( + isCritical: Boolean, + keyAlgorithm: PublicKeyAlgorithm, + hashAlgorithm: HashAlgorithm, + hashData: ByteArray + ): BaseSignatureSubpackets + + fun setSignatureTarget(signatureTarget: SignatureTarget?): BaseSignatureSubpackets + + fun setTrust(depth: Int, amount: Int): BaseSignatureSubpackets + + fun setTrust(isCritical: Boolean, depth: Int, amount: Int): BaseSignatureSubpackets + + fun setTrust(trust: TrustSignature?): BaseSignatureSubpackets + + @Throws(IOException::class) + fun addEmbeddedSignature(signature: PGPSignature): BaseSignatureSubpackets + + @Throws(IOException::class) + fun addEmbeddedSignature(isCritical: Boolean, signature: PGPSignature): BaseSignatureSubpackets + + fun addEmbeddedSignature(embeddedSignature: EmbeddedSignature): BaseSignatureSubpackets + + fun clearEmbeddedSignatures(): BaseSignatureSubpackets + + companion object { + + /** Factory method for a [Callback] that does nothing. */ + @JvmStatic fun nop() = object : Callback {} + + /** + * Factory function with receiver, which returns a [Callback] that modifies the hashed + * subpacket area of a [BaseSignatureSubpackets] object. + * + * Can be called like this: + * ``` + * val callback = BaseSignatureSubpackets.applyHashed { + * setCreationTime(date) + * ... + * } + * ``` + */ + @JvmStatic + fun applyHashed(function: BaseSignatureSubpackets.() -> Unit): Callback { + return object : Callback { + override fun modifyHashedSubpackets(hashedSubpackets: BaseSignatureSubpackets) { + function(hashedSubpackets) + } + } + } + + /** + * Factory function with receiver, which returns a [Callback] that modifies the unhashed + * subpacket area of a [BaseSignatureSubpackets] object. + * + * Can be called like this: + * ``` + * val callback = BaseSignatureSubpackets.applyUnhashed { + * setCreationTime(date) + * ... + * } + * ``` + */ + @JvmStatic + fun applyUnhashed(function: BaseSignatureSubpackets.() -> Unit): Callback { + return object : Callback { + override fun modifyUnhashedSubpackets(unhashedSubpackets: BaseSignatureSubpackets) { + function(unhashedSubpackets) + } + } + } + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/CertificationSubpackets.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/CertificationSubpackets.kt new file mode 100644 index 00000000..bb1d6550 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/CertificationSubpackets.kt @@ -0,0 +1,58 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.subpackets + +interface CertificationSubpackets : BaseSignatureSubpackets { + + interface Callback : SignatureSubpacketCallback + + companion object { + + /** Factory method for a [Callback] that does nothing. */ + @JvmStatic fun nop() = object : Callback {} + + /** + * Factory function with receiver, which returns a [Callback] that modifies the hashed + * subpacket area of a [CertificationSubpackets] object. + * + * Can be called like this: + * ``` + * val callback = CertificationSubpackets.applyHashed { + * setCreationTime(date) + * ... + * } + * ``` + */ + @JvmStatic + fun applyHashed(function: CertificationSubpackets.() -> Unit): Callback { + return object : Callback { + override fun modifyHashedSubpackets(hashedSubpackets: CertificationSubpackets) { + function(hashedSubpackets) + } + } + } + + /** + * Factory function with receiver, which returns a [Callback] that modifies the unhashed + * subpacket area of a [CertificationSubpackets] object. + * + * Can be called like this: + * ``` + * val callback = CertificationSubpackets.applyUnhashed { + * setCreationTime(date) + * ... + * } + * ``` + */ + @JvmStatic + fun applyUnhashed(function: CertificationSubpackets.() -> Unit): Callback { + return object : Callback { + override fun modifyUnhashedSubpackets(unhashedSubpackets: CertificationSubpackets) { + function(unhashedSubpackets) + } + } + } + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/RevocationSignatureSubpackets.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/RevocationSignatureSubpackets.kt new file mode 100644 index 00000000..79807322 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/RevocationSignatureSubpackets.kt @@ -0,0 +1,82 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.subpackets + +import org.bouncycastle.bcpg.sig.RevocationReason +import org.pgpainless.key.util.RevocationAttributes + +interface RevocationSignatureSubpackets : BaseSignatureSubpackets { + + interface Callback : SignatureSubpacketCallback + + fun setRevocationReason( + revocationAttributes: RevocationAttributes + ): RevocationSignatureSubpackets + + fun setRevocationReason( + isCritical: Boolean, + revocationAttributes: RevocationAttributes + ): RevocationSignatureSubpackets + + fun setRevocationReason( + isCritical: Boolean, + reason: RevocationAttributes.Reason, + description: CharSequence + ): RevocationSignatureSubpackets + + fun setRevocationReason(reason: RevocationReason?): RevocationSignatureSubpackets + + companion object { + + /** Factory method for a [Callback] that does nothing. */ + @JvmStatic fun nop() = object : Callback {} + + /** + * Factory function with receiver, which returns a [Callback] that modifies the hashed + * subpacket area of a [RevocationSignatureSubpackets] object. + * + * Can be called like this: + * ``` + * val callback = RevocationSignatureSubpackets.applyHashed { + * setCreationTime(date) + * ... + * } + * ``` + */ + @JvmStatic + fun applyHashed(function: RevocationSignatureSubpackets.() -> Unit): Callback { + return object : Callback { + override fun modifyHashedSubpackets( + hashedSubpackets: RevocationSignatureSubpackets + ) { + function(hashedSubpackets) + } + } + } + + /** + * Factory function with receiver, which returns a [Callback] that modifies the unhashed + * subpacket area of a [RevocationSignatureSubpackets] object. + * + * Can be called like this: + * ``` + * val callback = RevocationSignatureSubpackets.applyUnhashed { + * setCreationTime(date) + * ... + * } + * ``` + */ + @JvmStatic + fun applyUnhashed(function: RevocationSignatureSubpackets.() -> Unit): Callback { + return object : Callback { + override fun modifyUnhashedSubpackets( + unhashedSubpackets: RevocationSignatureSubpackets + ) { + function(unhashedSubpackets) + } + } + } + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SelfSignatureSubpackets.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SelfSignatureSubpackets.kt new file mode 100644 index 00000000..318b7adf --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SelfSignatureSubpackets.kt @@ -0,0 +1,182 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.subpackets + +import java.util.* +import org.bouncycastle.bcpg.sig.Features +import org.bouncycastle.bcpg.sig.KeyExpirationTime +import org.bouncycastle.bcpg.sig.KeyFlags +import org.bouncycastle.bcpg.sig.PreferredAlgorithms +import org.bouncycastle.bcpg.sig.PrimaryUserID +import org.bouncycastle.bcpg.sig.RevocationKey +import org.bouncycastle.openpgp.PGPPublicKey +import org.pgpainless.algorithm.* + +interface SelfSignatureSubpackets : BaseSignatureSubpackets { + + interface Callback : SignatureSubpacketCallback { + fun then(nextCallback: SignatureSubpacketCallback): Callback { + val currCallback = this + return object : Callback { + override fun modifyHashedSubpackets(hashedSubpackets: SelfSignatureSubpackets) { + currCallback.modifyHashedSubpackets(hashedSubpackets) + nextCallback.modifyHashedSubpackets(hashedSubpackets) + } + + override fun modifyUnhashedSubpackets(unhashedSubpackets: SelfSignatureSubpackets) { + currCallback.modifyUnhashedSubpackets(unhashedSubpackets) + nextCallback.modifyUnhashedSubpackets(unhashedSubpackets) + } + } + } + } + + fun setKeyFlags(vararg keyflags: KeyFlag): SelfSignatureSubpackets + + fun setKeyFlags(keyFlags: List): SelfSignatureSubpackets + + fun setKeyFlags(isCritical: Boolean, vararg keyFlags: KeyFlag): SelfSignatureSubpackets + + fun setKeyFlags(keyFlags: KeyFlags?): SelfSignatureSubpackets + + fun setPrimaryUserId(): SelfSignatureSubpackets + + fun setPrimaryUserId(isCritical: Boolean): SelfSignatureSubpackets + + fun setPrimaryUserId(primaryUserID: PrimaryUserID?): SelfSignatureSubpackets + + fun setKeyExpirationTime(key: PGPPublicKey, keyExpirationTime: Date?): SelfSignatureSubpackets + + fun setKeyExpirationTime( + keyCreationTime: Date, + keyExpirationTime: Date? + ): SelfSignatureSubpackets + + fun setKeyExpirationTime( + isCritical: Boolean, + keyCreationTime: Date, + keyExpirationTime: Date? + ): SelfSignatureSubpackets + + fun setKeyExpirationTime( + isCritical: Boolean, + secondsFromCreationToExpiration: Long + ): SelfSignatureSubpackets + + fun setKeyExpirationTime(keyExpirationTime: KeyExpirationTime?): SelfSignatureSubpackets + + fun setPreferredCompressionAlgorithms( + vararg algorithms: CompressionAlgorithm + ): SelfSignatureSubpackets + + fun setPreferredCompressionAlgorithms( + algorithms: Collection + ): SelfSignatureSubpackets + + fun setPreferredCompressionAlgorithms( + isCritical: Boolean, + algorithms: Collection + ): SelfSignatureSubpackets + + fun setPreferredCompressionAlgorithms( + preferredAlgorithms: PreferredAlgorithms? + ): SelfSignatureSubpackets + + fun setPreferredSymmetricKeyAlgorithms( + vararg algorithms: SymmetricKeyAlgorithm + ): SelfSignatureSubpackets + + fun setPreferredSymmetricKeyAlgorithms( + algorithms: Collection + ): SelfSignatureSubpackets + + fun setPreferredSymmetricKeyAlgorithms( + isCritical: Boolean, + algorithms: Collection + ): SelfSignatureSubpackets + + fun setPreferredSymmetricKeyAlgorithms( + algorithms: PreferredAlgorithms? + ): SelfSignatureSubpackets + + fun setPreferredHashAlgorithms(vararg algorithms: HashAlgorithm): SelfSignatureSubpackets + + fun setPreferredHashAlgorithms(algorithms: Collection): SelfSignatureSubpackets + + fun setPreferredHashAlgorithms( + isCritical: Boolean, + algorithms: Collection + ): SelfSignatureSubpackets + + fun setPreferredHashAlgorithms(algorithms: PreferredAlgorithms?): SelfSignatureSubpackets + + fun addRevocationKey(revocationKey: PGPPublicKey): SelfSignatureSubpackets + + fun addRevocationKey(isCritical: Boolean, revocationKey: PGPPublicKey): SelfSignatureSubpackets + + fun addRevocationKey( + isCritical: Boolean, + isSensitive: Boolean, + revocationKey: PGPPublicKey + ): SelfSignatureSubpackets + + fun addRevocationKey(revocationKey: RevocationKey): SelfSignatureSubpackets + + fun clearRevocationKeys(): SelfSignatureSubpackets + + fun setFeatures(vararg features: Feature): SelfSignatureSubpackets + + fun setFeatures(isCritical: Boolean, vararg features: Feature): SelfSignatureSubpackets + + fun setFeatures(features: Features?): SelfSignatureSubpackets + + companion object { + + /** Factory method for a [Callback] that does nothing. */ + @JvmStatic fun nop() = object : Callback {} + + /** + * Factory function with receiver, which returns a [Callback] that modifies the hashed + * subpacket area of a [SelfSignatureSubpackets] object. + * + * Can be called like this: + * ``` + * val callback = SelfSignatureSubpackets.applyHashed { + * setCreationTime(date) + * ... + * } + * ``` + */ + @JvmStatic + fun applyHashed(function: SelfSignatureSubpackets.() -> Unit): Callback { + return object : Callback { + override fun modifyHashedSubpackets(hashedSubpackets: SelfSignatureSubpackets) { + function(hashedSubpackets) + } + } + } + + /** + * Factory function with receiver, which returns a [Callback] that modifies the unhashed + * subpacket area of a [SelfSignatureSubpackets] object. + * + * Can be called like this: + * ``` + * val callback = SelfSignatureSubpackets.applyUnhashed { + * setCreationTime(date) + * ... + * } + * ``` + */ + @JvmStatic + fun applyUnhashed(function: SelfSignatureSubpackets.() -> Unit): Callback { + return object : Callback { + override fun modifyUnhashedSubpackets(unhashedSubpackets: SelfSignatureSubpackets) { + function(unhashedSubpackets) + } + } + } + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpacketCallback.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpacketCallback.kt new file mode 100644 index 00000000..fbc56035 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpacketCallback.kt @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.subpackets + +interface SignatureSubpacketCallback { + + /** + * Callback method that can be used to modify the hashed subpackets of a signature. + * + * @param hashedSubpackets hashed subpackets + */ + fun modifyHashedSubpackets(hashedSubpackets: S) { + // Empty default implementation to allow for cleaner overriding + } + + /** + * Callback method that can be used to modify the unhashed subpackets of a signature. + * + * @param unhashedSubpackets unhashed subpackets + */ + fun modifyUnhashedSubpackets(unhashedSubpackets: S) { + // Empty default implementation to allow for cleaner overriding + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpackets.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpackets.kt new file mode 100644 index 00000000..886cedb6 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpackets.kt @@ -0,0 +1,639 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.subpackets + +import java.lang.IllegalArgumentException +import java.net.URL +import java.util.* +import kotlin.experimental.or +import openpgp.secondsTill +import openpgp.toSecondsPrecision +import org.bouncycastle.bcpg.SignatureSubpacket +import org.bouncycastle.bcpg.SignatureSubpacketTags +import org.bouncycastle.bcpg.sig.* +import org.bouncycastle.openpgp.PGPPublicKey +import org.bouncycastle.openpgp.PGPSignature +import org.bouncycastle.openpgp.PGPSignatureSubpacketVector +import org.pgpainless.algorithm.* +import org.pgpainless.key.util.RevocationAttributes + +class SignatureSubpackets : + BaseSignatureSubpackets, + SelfSignatureSubpackets, + CertificationSubpackets, + RevocationSignatureSubpackets { + + interface Callback : SignatureSubpacketCallback + + var signatureCreationTimeSubpacket: SignatureCreationTime? = null + var signatureExpirationTimeSubpacket: SignatureExpirationTime? = null + var issuerKeyIdSubpacket: IssuerKeyID? = null + var issuerFingerprintSubpacket: IssuerFingerprint? = null + val notationDataSubpackets: List = mutableListOf() + val intendedRecipientFingerprintSubpackets: List = mutableListOf() + val revocationKeySubpackets: List = mutableListOf() + var exportableSubpacket: Exportable? = null + var signatureTargetSubpacket: SignatureTarget? = null + var featuresSubpacket: Features? = null + var keyFlagsSubpacket: KeyFlags? = null + var trustSubpacket: TrustSignature? = null + var preferredCompressionAlgorithmsSubpacket: PreferredAlgorithms? = null + var preferredSymmetricKeyAlgorithmsSubpacket: PreferredAlgorithms? = null + var preferredHashAlgorithmsSubpacket: PreferredAlgorithms? = null + val embeddedSignatureSubpackets: List = mutableListOf() + var signerUserIdSubpacket: SignerUserID? = null + var keyExpirationTimeSubpacket: KeyExpirationTime? = null + var policyURISubpacket: PolicyURI? = null + var primaryUserIdSubpacket: PrimaryUserID? = null + var regularExpressionSubpacket: RegularExpression? = null + var revocableSubpacket: Revocable? = null + var revocationReasonSubpacket: RevocationReason? = null + val residualSubpackets: List = mutableListOf() + + companion object { + + @JvmStatic + fun refreshHashedSubpackets( + issuer: PGPPublicKey, + oldSignature: PGPSignature + ): SignatureSubpackets { + return createHashedSubpacketsFrom(issuer, oldSignature.hashedSubPackets) + } + + @JvmStatic + fun refreshUnhashedSubpackets(oldSignature: PGPSignature): SignatureSubpackets { + return createSubpacketsFrom(oldSignature.unhashedSubPackets) + } + + @JvmStatic + fun createHashedSubpacketsFrom( + issuer: PGPPublicKey, + base: PGPSignatureSubpacketVector + ): SignatureSubpackets { + return createSubpacketsFrom(base).apply { setIssuerFingerprintAndKeyId(issuer) } + } + + @JvmStatic + fun createSubpacketsFrom(base: PGPSignatureSubpacketVector): SignatureSubpackets { + return SignatureSubpacketsHelper.applyFrom(base, SignatureSubpackets()) + } + + @JvmStatic + fun createHashedSubpackets(issuer: PGPPublicKey): SignatureSubpackets { + return createEmptySubpackets().setIssuerFingerprintAndKeyId(issuer) + } + + @JvmStatic + fun createEmptySubpackets(): SignatureSubpackets { + return SignatureSubpackets() + } + + /** Factory method for a [Callback] that does nothing. */ + @JvmStatic fun nop() = object : Callback {} + + /** + * Factory function with receiver, which returns a [Callback] that modifies the hashed + * subpacket area of a [SignatureSubpackets] object. + * + * Can be called like this: + * ``` + * val callback = SignatureSubpackets.applyHashed { + * setCreationTime(date) + * ... + * } + * ``` + */ + @JvmStatic + fun applyHashed(function: SignatureSubpackets.() -> Unit): Callback { + return object : Callback { + override fun modifyHashedSubpackets(hashedSubpackets: SignatureSubpackets) { + function(hashedSubpackets) + } + } + } + + /** + * Factory function with receiver, which returns a [Callback] that modifies the unhashed + * subpacket area of a [SignatureSubpackets] object. + * + * Can be called like this: + * ``` + * val callback = SignatureSubpackets.applyUnhashed { + * setCreationTime(date) + * ... + * } + * ``` + */ + @JvmStatic + fun applyUnhashed(function: SignatureSubpackets.() -> Unit): Callback { + return object : Callback { + override fun modifyUnhashedSubpackets(unhashedSubpackets: SignatureSubpackets) { + function(unhashedSubpackets) + } + } + } + } + + override fun setRevocationReason( + revocationAttributes: RevocationAttributes + ): SignatureSubpackets = apply { setRevocationReason(false, revocationAttributes) } + + override fun setRevocationReason( + isCritical: Boolean, + revocationAttributes: RevocationAttributes + ): SignatureSubpackets = apply { + setRevocationReason( + isCritical, revocationAttributes.reason, revocationAttributes.description) + } + + override fun setRevocationReason( + isCritical: Boolean, + reason: RevocationAttributes.Reason, + description: CharSequence + ): SignatureSubpackets = apply { + setRevocationReason(RevocationReason(isCritical, reason.code, description.toString())) + } + + override fun setRevocationReason(reason: RevocationReason?): SignatureSubpackets = apply { + this.revocationReasonSubpacket = reason + } + + override fun setKeyFlags(vararg keyflags: KeyFlag): SignatureSubpackets = apply { + setKeyFlags(true, *keyflags) + } + + override fun setKeyFlags(keyFlags: List): SignatureSubpackets = apply { + setKeyFlags(true, *keyFlags.toTypedArray()) + } + + override fun setKeyFlags(isCritical: Boolean, vararg keyFlags: KeyFlag): SignatureSubpackets = + apply { + setKeyFlags(KeyFlags(isCritical, KeyFlag.toBitmask(*keyFlags))) + } + + override fun setKeyFlags(keyFlags: KeyFlags?): SignatureSubpackets = apply { + this.keyFlagsSubpacket = keyFlags + } + + override fun setPrimaryUserId(): SignatureSubpackets = apply { setPrimaryUserId(true) } + + override fun setPrimaryUserId(isCritical: Boolean): SignatureSubpackets = apply { + setPrimaryUserId(PrimaryUserID(isCritical, true)) + } + + override fun setPrimaryUserId(primaryUserID: PrimaryUserID?): SignatureSubpackets = apply { + this.primaryUserIdSubpacket = primaryUserID + } + + override fun setKeyExpirationTime( + key: PGPPublicKey, + keyExpirationTime: Date? + ): SignatureSubpackets = apply { setKeyExpirationTime(key.creationTime, keyExpirationTime) } + + override fun setKeyExpirationTime( + keyCreationTime: Date, + keyExpirationTime: Date? + ): SignatureSubpackets = apply { + setKeyExpirationTime(true, keyCreationTime, keyExpirationTime) + } + + override fun setKeyExpirationTime( + isCritical: Boolean, + keyCreationTime: Date, + keyExpirationTime: Date? + ): SignatureSubpackets = apply { + if (keyExpirationTime == null) { + setKeyExpirationTime(isCritical, 0) + } else { + setKeyExpirationTime(isCritical, keyCreationTime.secondsTill(keyExpirationTime)) + } + } + + override fun setKeyExpirationTime( + isCritical: Boolean, + secondsFromCreationToExpiration: Long + ): SignatureSubpackets = apply { + enforceExpirationBounds(secondsFromCreationToExpiration) + setKeyExpirationTime(KeyExpirationTime(isCritical, secondsFromCreationToExpiration)) + } + + override fun setKeyExpirationTime(keyExpirationTime: KeyExpirationTime?): SignatureSubpackets = + apply { + this.keyExpirationTimeSubpacket = keyExpirationTime + } + + override fun setPreferredCompressionAlgorithms( + vararg algorithms: CompressionAlgorithm + ): SignatureSubpackets = apply { setPreferredCompressionAlgorithms(setOf(*algorithms)) } + + override fun setPreferredCompressionAlgorithms( + algorithms: Collection + ): SignatureSubpackets = apply { setPreferredCompressionAlgorithms(false, algorithms) } + + override fun setPreferredCompressionAlgorithms( + isCritical: Boolean, + algorithms: Collection + ): SignatureSubpackets = apply { + setPreferredCompressionAlgorithms( + PreferredAlgorithms( + SignatureSubpacketTags.PREFERRED_COMP_ALGS, + isCritical, + algorithms.map { it.algorithmId }.toIntArray())) + } + + override fun setPreferredCompressionAlgorithms( + algorithms: PreferredAlgorithms? + ): SignatureSubpackets = apply { + require( + algorithms == null || algorithms.type == SignatureSubpacketTags.PREFERRED_COMP_ALGS) { + "Invalid preferred compression algorithms type." + } + this.preferredCompressionAlgorithmsSubpacket = algorithms + } + + override fun setPreferredSymmetricKeyAlgorithms( + vararg algorithms: SymmetricKeyAlgorithm + ): SignatureSubpackets = apply { setPreferredSymmetricKeyAlgorithms(setOf(*algorithms)) } + + override fun setPreferredSymmetricKeyAlgorithms( + algorithms: Collection + ): SignatureSubpackets = apply { setPreferredSymmetricKeyAlgorithms(false, algorithms) } + + override fun setPreferredSymmetricKeyAlgorithms( + isCritical: Boolean, + algorithms: Collection + ): SignatureSubpackets = apply { + setPreferredSymmetricKeyAlgorithms( + PreferredAlgorithms( + SignatureSubpacketTags.PREFERRED_SYM_ALGS, + isCritical, + algorithms.map { it.algorithmId }.toIntArray())) + } + + override fun setPreferredSymmetricKeyAlgorithms( + algorithms: PreferredAlgorithms? + ): SignatureSubpackets = apply { + require( + algorithms == null || algorithms.type == SignatureSubpacketTags.PREFERRED_SYM_ALGS) { + "Invalid preferred symmetric algorithms type." + } + this.preferredSymmetricKeyAlgorithmsSubpacket = algorithms + } + + override fun setPreferredHashAlgorithms(vararg algorithms: HashAlgorithm): SignatureSubpackets = + apply { + setPreferredHashAlgorithms(setOf(*algorithms)) + } + + override fun setPreferredHashAlgorithms( + algorithms: Collection + ): SignatureSubpackets = apply { setPreferredHashAlgorithms(false, algorithms) } + + override fun setPreferredHashAlgorithms( + isCritical: Boolean, + algorithms: Collection + ): SignatureSubpackets = apply { + setPreferredHashAlgorithms( + PreferredAlgorithms( + SignatureSubpacketTags.PREFERRED_HASH_ALGS, + isCritical, + algorithms.map { it.algorithmId }.toIntArray())) + } + + override fun setPreferredHashAlgorithms(algorithms: PreferredAlgorithms?): SignatureSubpackets = + apply { + require( + algorithms == null || + algorithms.type == SignatureSubpacketTags.PREFERRED_HASH_ALGS) { + "Invalid preferred hash algorithms type." + } + this.preferredHashAlgorithmsSubpacket = algorithms + } + + override fun addRevocationKey(revocationKey: PGPPublicKey): SignatureSubpackets = apply { + addRevocationKey(true, revocationKey) + } + + override fun addRevocationKey( + isCritical: Boolean, + revocationKey: PGPPublicKey + ): SignatureSubpackets = apply { addRevocationKey(isCritical, false, revocationKey) } + + override fun addRevocationKey( + isCritical: Boolean, + isSensitive: Boolean, + revocationKey: PGPPublicKey + ): SignatureSubpackets = apply { + val clazz = 0x80.toByte() or if (isSensitive) 0x40.toByte() else 0x00.toByte() + addRevocationKey( + RevocationKey(isCritical, clazz, revocationKey.algorithm, revocationKey.fingerprint)) + } + + override fun addRevocationKey(revocationKey: RevocationKey): SignatureSubpackets = apply { + (this.revocationKeySubpackets as MutableList).add(revocationKey) + } + + override fun clearRevocationKeys(): SignatureSubpackets = apply { + (this.revocationKeySubpackets as MutableList).clear() + } + + override fun setFeatures(vararg features: Feature): SignatureSubpackets = apply { + setFeatures(true, *features) + } + + override fun setFeatures(isCritical: Boolean, vararg features: Feature): SignatureSubpackets = + apply { + setFeatures(Features(isCritical, Feature.toBitmask(*features))) + } + + override fun setFeatures(features: Features?): SignatureSubpackets = apply { + this.featuresSubpacket = features + } + + override fun setIssuerFingerprintAndKeyId(key: PGPPublicKey): SignatureSubpackets = apply { + setIssuerKeyId(key.keyID) + setIssuerFingerprint(key) + } + + override fun setIssuerKeyId(keyId: Long): SignatureSubpackets = apply { + setIssuerKeyId(false, keyId) + } + + override fun setIssuerKeyId(isCritical: Boolean, keyId: Long): SignatureSubpackets = apply { + setIssuerKeyId(IssuerKeyID(isCritical, keyId)) + } + + override fun setIssuerKeyId(issuerKeyID: IssuerKeyID?): SignatureSubpackets = apply { + this.issuerKeyIdSubpacket = issuerKeyID + } + + override fun setIssuerFingerprint( + isCritical: Boolean, + issuer: PGPPublicKey + ): SignatureSubpackets = apply { + setIssuerFingerprint(IssuerFingerprint(isCritical, issuer.version, issuer.fingerprint)) + } + + override fun setIssuerFingerprint(issuer: PGPPublicKey): SignatureSubpackets = apply { + setIssuerFingerprint(false, issuer) + } + + override fun setIssuerFingerprint(fingerprint: IssuerFingerprint?): SignatureSubpackets = + apply { + this.issuerFingerprintSubpacket = fingerprint + } + + override fun setSignatureCreationTime(creationTime: Date): SignatureSubpackets = apply { + setSignatureCreationTime(true, creationTime) + } + + override fun setSignatureCreationTime( + isCritical: Boolean, + creationTime: Date + ): SignatureSubpackets = apply { + setSignatureCreationTime(SignatureCreationTime(isCritical, creationTime)) + } + + override fun setSignatureCreationTime( + creationTime: SignatureCreationTime? + ): SignatureSubpackets = apply { this.signatureCreationTimeSubpacket = creationTime } + + override fun setSignatureExpirationTime( + creationTime: Date, + expirationTime: Date? + ): SignatureSubpackets = apply { + setSignatureExpirationTime(true, creationTime, expirationTime) + } + + override fun setSignatureExpirationTime( + isCritical: Boolean, + creationTime: Date, + expirationTime: Date? + ): SignatureSubpackets = apply { + if (expirationTime != null) { + require(creationTime.toSecondsPrecision() < expirationTime.toSecondsPrecision()) { + "Expiration time MUST NOT be less or equal the creation time." + } + setSignatureExpirationTime( + SignatureExpirationTime(isCritical, creationTime.secondsTill(expirationTime))) + } else { + setSignatureExpirationTime(SignatureExpirationTime(isCritical, 0)) + } + } + + override fun setSignatureExpirationTime( + isCritical: Boolean, + seconds: Long + ): SignatureSubpackets = apply { + enforceExpirationBounds(seconds) + setSignatureExpirationTime(SignatureExpirationTime(isCritical, seconds)) + } + + /** + * Enforce that
seconds
is within bounds of an unsigned 32bit number. Values less + * than 0 are illegal, as well as values greater 0xffffffff. + * + * @param seconds number to check + * @throws IllegalArgumentException in case of an under- or overflow + */ + private fun enforceExpirationBounds(seconds: Long) { + require(seconds <= 0xffffffffL) { + "Integer overflow. Seconds from creation to expiration (${seconds}) cannot be larger than ${0xffffffffL}." + } + require(seconds >= 0) { "Seconds from creation to expiration cannot be less than 0." } + } + + override fun setSignatureExpirationTime( + expirationTime: SignatureExpirationTime? + ): SignatureSubpackets = apply { this.signatureExpirationTimeSubpacket = expirationTime } + + override fun setSignerUserId(userId: CharSequence): SignatureSubpackets = apply { + setSignerUserId(false, userId) + } + + override fun setSignerUserId(isCritical: Boolean, userId: CharSequence): SignatureSubpackets = + apply { + setSignerUserId(SignerUserID(isCritical, userId.toString())) + } + + override fun setSignerUserId(signerUserID: SignerUserID?): SignatureSubpackets = apply { + this.signerUserIdSubpacket = signerUserID + } + + override fun addNotationData( + isCritical: Boolean, + notationName: String, + notationValue: String + ): SignatureSubpackets = apply { + addNotationData(isCritical, true, notationName, notationValue) + } + + override fun addNotationData( + isCritical: Boolean, + isHumanReadable: Boolean, + notationName: String, + notationValue: String + ): SignatureSubpackets = apply { + addNotationData(NotationData(isCritical, isHumanReadable, notationName, notationValue)) + } + + override fun addNotationData(notationData: NotationData): SignatureSubpackets = apply { + (this.notationDataSubpackets as MutableList).add(notationData) + } + + override fun clearNotationData(): SignatureSubpackets = apply { + (this.notationDataSubpackets as MutableList).clear() + } + + override fun addIntendedRecipientFingerprint(recipientKey: PGPPublicKey): SignatureSubpackets = + apply { + addIntendedRecipientFingerprint(false, recipientKey) + } + + override fun addIntendedRecipientFingerprint( + isCritical: Boolean, + recipientKey: PGPPublicKey + ): SignatureSubpackets = apply { + addIntendedRecipientFingerprint( + IntendedRecipientFingerprint( + isCritical, recipientKey.version, recipientKey.fingerprint)) + } + + override fun addIntendedRecipientFingerprint( + intendedRecipient: IntendedRecipientFingerprint + ): SignatureSubpackets = apply { + (this.intendedRecipientFingerprintSubpackets as MutableList).add(intendedRecipient) + } + + override fun clearIntendedRecipientFingerprints(): SignatureSubpackets = apply { + (this.intendedRecipientFingerprintSubpackets as MutableList).clear() + } + + override fun setExportable(): SignatureSubpackets = apply { setExportable(true) } + + override fun setExportable(isExportable: Boolean): SignatureSubpackets = apply { + setExportable(true, isExportable) + } + + override fun setExportable(isCritical: Boolean, isExportable: Boolean): SignatureSubpackets = + apply { + setExportable(Exportable(isCritical, isExportable)) + } + + override fun setExportable(exportable: Exportable?): SignatureSubpackets = apply { + this.exportableSubpacket = exportable + } + + override fun setPolicyUrl(policyUrl: URL): SignatureSubpackets = apply { + setPolicyUrl(false, policyUrl) + } + + override fun setPolicyUrl(isCritical: Boolean, policyUrl: URL): SignatureSubpackets = apply { + setPolicyUrl(PolicyURI(isCritical, policyUrl.toString())) + } + + override fun setPolicyUrl(policyUrl: PolicyURI?): SignatureSubpackets = apply { + this.policyURISubpacket = policyURISubpacket + } + + override fun setRegularExpression(regex: CharSequence): SignatureSubpackets = apply { + setRegularExpression(false, regex) + } + + override fun setRegularExpression( + isCritical: Boolean, + regex: CharSequence + ): SignatureSubpackets = apply { + setRegularExpression(RegularExpression(isCritical, regex.toString())) + } + + override fun setRegularExpression(regex: RegularExpression?): SignatureSubpackets = apply { + this.regularExpressionSubpacket = regex + } + + override fun setRevocable(): SignatureSubpackets = apply { setRevocable(true) } + + override fun setRevocable(isRevocable: Boolean): SignatureSubpackets = apply { + setRevocable(true, isRevocable) + } + + override fun setRevocable(isCritical: Boolean, isRevocable: Boolean): SignatureSubpackets = + apply { + setRevocable(Revocable(isCritical, isRevocable)) + } + + override fun setRevocable(revocable: Revocable?): SignatureSubpackets = apply { + this.revocableSubpacket = revocable + } + + override fun setSignatureTarget( + keyAlgorithm: PublicKeyAlgorithm, + hashAlgorithm: HashAlgorithm, + hashData: ByteArray + ): SignatureSubpackets = apply { + setSignatureTarget(true, keyAlgorithm, hashAlgorithm, hashData) + } + + override fun setSignatureTarget( + isCritical: Boolean, + keyAlgorithm: PublicKeyAlgorithm, + hashAlgorithm: HashAlgorithm, + hashData: ByteArray + ): SignatureSubpackets = apply { + setSignatureTarget( + SignatureTarget( + isCritical, keyAlgorithm.algorithmId, hashAlgorithm.algorithmId, hashData)) + } + + override fun setSignatureTarget(signatureTarget: SignatureTarget?): SignatureSubpackets = + apply { + this.signatureTargetSubpacket = signatureTarget + } + + override fun setTrust(depth: Int, amount: Int): SignatureSubpackets = apply { + setTrust(true, depth, amount) + } + + override fun setTrust(isCritical: Boolean, depth: Int, amount: Int): SignatureSubpackets = + apply { + setTrust(TrustSignature(isCritical, depth, amount)) + } + + override fun setTrust(trust: TrustSignature?): SignatureSubpackets = apply { + this.trustSubpacket = trust + } + + override fun addEmbeddedSignature(signature: PGPSignature): SignatureSubpackets = apply { + addEmbeddedSignature(true, signature) + } + + override fun addEmbeddedSignature( + isCritical: Boolean, + signature: PGPSignature + ): SignatureSubpackets = apply { + val sig = signature.encoded + val data = + if (sig.size - 1 > 256) { + ByteArray(sig.size - 3) + } else { + ByteArray(sig.size - 2) + } + System.arraycopy(sig, sig.size - data.size, data, 0, data.size) + addEmbeddedSignature(EmbeddedSignature(isCritical, false, data)) + } + + override fun addEmbeddedSignature(embeddedSignature: EmbeddedSignature): SignatureSubpackets = + apply { + (this.embeddedSignatureSubpackets as MutableList).add(embeddedSignature) + } + + override fun clearEmbeddedSignatures(): SignatureSubpackets = apply { + (this.embeddedSignatureSubpackets as MutableList).clear() + } + + fun addResidualSubpacket( + subpacket: org.bouncycastle.bcpg.SignatureSubpacket + ): SignatureSubpackets = apply { (residualSubpackets as MutableList).add(subpacket) } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpacketsHelper.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpacketsHelper.kt new file mode 100644 index 00000000..8a6c16bf --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpacketsHelper.kt @@ -0,0 +1,210 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.subpackets + +import org.bouncycastle.bcpg.sig.* +import org.bouncycastle.openpgp.PGPSignatureSubpacketGenerator +import org.bouncycastle.openpgp.PGPSignatureSubpacketVector +import org.pgpainless.algorithm.* +import org.pgpainless.key.util.RevocationAttributes + +class SignatureSubpacketsHelper { + + companion object { + @JvmStatic + fun applyFrom(vector: PGPSignatureSubpacketVector, subpackets: SignatureSubpackets) = + subpackets.apply { + for (subpacket in vector.toArray()) { + val type = SignatureSubpacket.fromCode(subpacket.type) + when (type) { + SignatureSubpacket.signatureCreationTime, + SignatureSubpacket.issuerKeyId, + SignatureSubpacket.issuerFingerprint -> { + /* ignore, we override this anyway */ + } + SignatureSubpacket.signatureExpirationTime -> + (subpacket as SignatureExpirationTime).let { + subpackets.setSignatureExpirationTime(it.isCritical, it.time) + } + SignatureSubpacket.exportableCertification -> + (subpacket as Exportable).let { + subpackets.setExportable(it.isCritical, it.isExportable) + } + SignatureSubpacket.trustSignature -> + (subpacket as TrustSignature).let { + subpackets.setTrust(it.isCritical, it.depth, it.trustAmount) + } + SignatureSubpacket.revocable -> + (subpacket as Revocable).let { + subpackets.setRevocable(it.isCritical, it.isRevocable) + } + SignatureSubpacket.keyExpirationTime -> + (subpacket as KeyExpirationTime).let { + subpackets.setKeyExpirationTime(it.isCritical, it.time) + } + SignatureSubpacket.preferredSymmetricAlgorithms -> + (subpacket as PreferredAlgorithms).let { + subpackets.setPreferredSymmetricKeyAlgorithms( + PreferredAlgorithms( + it.type, it.isCritical, it.isLongLength, it.data)) + } + SignatureSubpacket.preferredHashAlgorithms -> + (subpacket as PreferredAlgorithms).let { + subpackets.setPreferredHashAlgorithms( + PreferredAlgorithms( + it.type, it.isCritical, it.isLongLength, it.data)) + } + SignatureSubpacket.preferredCompressionAlgorithms -> + (subpacket as PreferredAlgorithms).let { + subpackets.setPreferredCompressionAlgorithms( + PreferredAlgorithms( + it.type, it.isCritical, it.isLongLength, it.data)) + } + SignatureSubpacket.revocationKey -> + (subpacket as RevocationKey).let { + subpackets.addRevocationKey( + RevocationKey( + it.isCritical, + it.signatureClass, + it.algorithm, + it.fingerprint)) + } + SignatureSubpacket.notationData -> + (subpacket as NotationData).let { + subpackets.addNotationData( + it.isCritical, + it.isHumanReadable, + it.notationName, + it.notationValue) + } + SignatureSubpacket.primaryUserId -> + (subpacket as PrimaryUserID).let { + subpackets.setPrimaryUserId( + PrimaryUserID(it.isCritical, it.isPrimaryUserID)) + } + SignatureSubpacket.keyFlags -> + (subpacket as KeyFlags).let { + subpackets.setKeyFlags( + it.isCritical, *(KeyFlag.fromBitmask(it.flags).toTypedArray())) + } + SignatureSubpacket.signerUserId -> + (subpacket as SignerUserID).let { + subpackets.setSignerUserId(it.isCritical, it.id) + } + SignatureSubpacket.revocationReason -> + (subpacket as RevocationReason).let { + subpackets.setRevocationReason( + it.isCritical, + RevocationAttributes.Reason.fromCode(it.revocationReason), + it.revocationDescription) + } + SignatureSubpacket.features -> + (subpacket as Features).let { + subpackets.setFeatures( + it.isCritical, + *(Feature.fromBitmask(it.features.toInt()).toTypedArray())) + } + SignatureSubpacket.signatureTarget -> + (subpacket as SignatureTarget).let { + subpackets.setSignatureTarget( + it.isCritical, + PublicKeyAlgorithm.requireFromId(it.publicKeyAlgorithm), + HashAlgorithm.requireFromId(it.hashAlgorithm), + it.hashData) + } + SignatureSubpacket.embeddedSignature -> + (subpacket as EmbeddedSignature).let { + subpackets.addEmbeddedSignature(it) + } + SignatureSubpacket.intendedRecipientFingerprint -> + (subpacket as IntendedRecipientFingerprint).let { + subpackets.addIntendedRecipientFingerprint(it) + } + SignatureSubpacket.policyUrl -> + (subpacket as PolicyURI).let { subpackets.setPolicyUrl(it) } + SignatureSubpacket.regularExpression -> + (subpacket as RegularExpression).let { + subpackets.setRegularExpression(it) + } + SignatureSubpacket.keyServerPreferences, + SignatureSubpacket.preferredKeyServers, + SignatureSubpacket.placeholder, + SignatureSubpacket.preferredAEADAlgorithms, + SignatureSubpacket.attestedCertification -> + subpackets.addResidualSubpacket(subpacket) + else -> subpackets.addResidualSubpacket(subpacket) + } + } + } + + @JvmStatic + fun applyTo( + subpackets: SignatureSubpackets, + generator: PGPSignatureSubpacketGenerator + ): PGPSignatureSubpacketGenerator { + return generator.apply { + addSubpacket(subpackets.issuerKeyIdSubpacket) + addSubpacket(subpackets.issuerFingerprintSubpacket) + addSubpacket(subpackets.signatureCreationTimeSubpacket) + addSubpacket(subpackets.signatureExpirationTimeSubpacket) + addSubpacket(subpackets.exportableSubpacket) + addSubpacket(subpackets.policyURISubpacket) + addSubpacket(subpackets.regularExpressionSubpacket) + for (notation in subpackets.notationDataSubpackets) { + addSubpacket(notation) + } + for (recipient in subpackets.intendedRecipientFingerprintSubpackets) { + addSubpacket(recipient) + } + for (revocationKey in subpackets.revocationKeySubpackets) { + addSubpacket(revocationKey) + } + addSubpacket(subpackets.signatureTargetSubpacket) + addSubpacket(subpackets.featuresSubpacket) + addSubpacket(subpackets.keyFlagsSubpacket) + addSubpacket(subpackets.trustSubpacket) + addSubpacket(subpackets.preferredCompressionAlgorithmsSubpacket) + addSubpacket(subpackets.preferredSymmetricKeyAlgorithmsSubpacket) + addSubpacket(subpackets.preferredHashAlgorithmsSubpacket) + for (embedded in subpackets.embeddedSignatureSubpackets) { + addSubpacket(embedded) + } + addSubpacket(subpackets.signerUserIdSubpacket) + addSubpacket(subpackets.keyExpirationTimeSubpacket) + addSubpacket(subpackets.primaryUserIdSubpacket) + addSubpacket(subpackets.revocableSubpacket) + addSubpacket(subpackets.revocationReasonSubpacket) + for (residual in subpackets.residualSubpackets) { + addSubpacket(residual) + } + } + } + + @JvmStatic + private fun PGPSignatureSubpacketGenerator.addSubpacket( + subpacket: org.bouncycastle.bcpg.SignatureSubpacket? + ) { + if (subpacket != null) { + this.addCustomSubpacket(subpacket) + } + } + + @JvmStatic + fun toVector(subpackets: SignatureSubpackets): PGPSignatureSubpacketVector { + return PGPSignatureSubpacketGenerator().let { + applyTo(subpackets, it) + it.generate() + } + } + + @JvmStatic + fun toVector(subpackets: RevocationSignatureSubpackets): PGPSignatureSubpacketVector { + return PGPSignatureSubpacketGenerator().let { + applyTo(subpackets as SignatureSubpackets, it) + it.generate() + } + } + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.kt new file mode 100644 index 00000000..dcc85630 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.kt @@ -0,0 +1,596 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.subpackets + +import java.util.* +import openpgp.openPgpKeyId +import openpgp.plusSeconds +import org.bouncycastle.bcpg.sig.* +import org.bouncycastle.openpgp.PGPPublicKey +import org.bouncycastle.openpgp.PGPSignature +import org.bouncycastle.openpgp.PGPSignatureList +import org.bouncycastle.openpgp.PGPSignatureSubpacketVector +import org.pgpainless.algorithm.* +import org.pgpainless.algorithm.KeyFlag.Companion.hasKeyFlag +import org.pgpainless.algorithm.KeyFlag.Companion.toBitmask +import org.pgpainless.key.OpenPgpFingerprint +import org.pgpainless.key.OpenPgpV4Fingerprint +import org.pgpainless.key.OpenPgpV5Fingerprint +import org.pgpainless.key.OpenPgpV6Fingerprint +import org.pgpainless.key.generation.type.KeyType + +class SignatureSubpacketsUtil { + companion object { + + /** + * Return the issuer-fingerprint subpacket of the signature. Since this packet is + * self-authenticating, we expect it to be in the unhashed area, however as it cannot hurt + * we search for it in the hashed area first. + * + * @param signature signature + * @return issuer fingerprint or null + */ + @JvmStatic + fun getIssuerFingerprint(signature: PGPSignature): IssuerFingerprint? = + hashedOrUnhashed(signature, SignatureSubpacket.issuerFingerprint) + + /** + * Return the [IssuerFingerprint] subpacket of the signature into a + * [org.pgpainless.key.OpenPgpFingerprint]. If no v4, v5 or v6 issuer fingerprint is present + * in the signature, return null. + * + * @param signature signature + * @return fingerprint of the issuer, or null + */ + @JvmStatic + fun getIssuerFingerprintAsOpenPgpFingerprint(signature: PGPSignature): OpenPgpFingerprint? { + val subpacket = getIssuerFingerprint(signature) ?: return null + return when (subpacket.keyVersion) { + 4 -> OpenPgpV4Fingerprint(subpacket.fingerprint) + 5 -> OpenPgpV5Fingerprint(subpacket.fingerprint) + 6 -> OpenPgpV6Fingerprint(subpacket.fingerprint) + else -> null + } + } + + @JvmStatic + fun getIssuerKeyId(signature: PGPSignature): IssuerKeyID? = + hashedOrUnhashed(signature, SignatureSubpacket.issuerKeyId) + + /** + * Inspect the given signature's [IssuerKeyID] packet to determine the issuer key-id. If no + * such packet is present, return null. + * + * @param signature signature + * @return issuer key-id as [Long] + */ + @JvmStatic + fun getIssuerKeyIdAsLong(signature: PGPSignature): Long? = getIssuerKeyId(signature)?.keyID + + /** + * Return the revocation reason subpacket of the signature. Since this packet is rather + * important for revocations, we only search for it in the hashed area of the signature. + * + * @param signature signature + * @return revocation reason + */ + @JvmStatic + fun getRevocationReason(signature: PGPSignature): RevocationReason? = + hashed(signature, SignatureSubpacket.revocationReason) + + /** + * Return the signature creation time subpacket. Since this packet is rather important, we + * only search for it in the hashed area of the signature. + * + * @param signature signature + * @return signature creation time subpacket + */ + @JvmStatic + fun getSignatureCreationTime(signature: PGPSignature): SignatureCreationTime? = + if (signature.version == 3) SignatureCreationTime(false, signature.creationTime) + else hashed(signature, SignatureSubpacket.signatureCreationTime) + + /** + * Return the signature expiration time subpacket of the signature. Since this packet is + * rather important, we only search for it in the hashed area of the signature. + * + * @param signature signature + * @return signature expiration time + */ + @JvmStatic + fun getSignatureExpirationTime(signature: PGPSignature): SignatureExpirationTime? = + hashed(signature, SignatureSubpacket.signatureExpirationTime) + + /** + * Return the signatures' expiration time as a date. The expiration date is computed by + * adding the expiration time to the signature creation date. If the signature has no + * expiration time subpacket, or the expiration time is set to '0', this message returns + * null. + * + * @param signature signature + * @return expiration time as date + */ + @JvmStatic + fun getSignatureExpirationTimeAsDate(signature: PGPSignature): Date? = + getSignatureExpirationTime(signature)?.let { + signature.creationTime.plusSeconds(it.time) + } + + /** + * Return the key expiration time subpacket of this signature. We only look for it in the + * hashed area of the signature. + * + * @param signature signature + * @return key expiration time + */ + @JvmStatic + fun getKeyExpirationTime(signature: PGPSignature): KeyExpirationTime? = + hashed(signature, SignatureSubpacket.keyExpirationTime) + + /** + * Return the signatures key-expiration time as a date. The expiration date is computed by + * adding the signatures' key-expiration time to the signing keys creation date. If the + * signature does not have a key-expiration time subpacket, or its value is '0', this method + * returns null. + * + * @param signature self-signature carrying the key-expiration time subpacket + * @param signingKey signature creation key + * @return key expiration time as date + */ + @JvmStatic + fun getKeyExpirationTimeAsDate(signature: PGPSignature, signingKey: PGPPublicKey): Date? = + require(signature.keyID == signingKey.keyID) { + "Provided key (${signingKey.keyID.openPgpKeyId()}) did not create the signature (${signature.keyID.openPgpKeyId()})" + } + .run { + getKeyExpirationTime(signature)?.let { + signingKey.creationTime.plusSeconds(it.time) + } + } + + /** + * Calculate the duration in seconds until the key expires after creation. + * + * @param expirationTime new expiration date + * @param creationTime key creation time + * @return lifetime of the key in seconds + */ + @JvmStatic + fun getKeyLifetimeInSeconds(creationTime: Date, expirationTime: Date?): Long = + expirationTime?.let { + require(creationTime <= it) { + "Key MUST NOT expire before being created.\n" + + "(creation: $creationTime, expiration: $it)" + } + .run { (it.time - creationTime.time) / 1000 } + } + ?: 0 // 0 means "no expiration" + + /** + * Return the revocable subpacket of this signature. We only look for it in the hashed area + * of the signature. + * + * @param signature signature + * @return revocable subpacket + */ + @JvmStatic + fun getRevocable(signature: PGPSignature): Revocable? = + hashed(signature, SignatureSubpacket.revocable) + + /** + * Return the symmetric algorithm preferences from the signatures hashed area. + * + * @param signature signature + * @return symm. algo. prefs + */ + @JvmStatic + fun getPreferredSymmetricAlgorithms(signature: PGPSignature): PreferredAlgorithms? = + hashed(signature, SignatureSubpacket.preferredSymmetricAlgorithms) + + /** + * Return the preferred [SymmetricKeyAlgorithms][SymmetricKeyAlgorithm] as present in the + * signature. If no preference is given with regard to symmetric encryption algorithms, + * return an empty set. + * + * In any case, the resulting set is ordered by occurrence. + * + * @param signature signature + * @return ordered set of symmetric key algorithm preferences + */ + @JvmStatic + fun parsePreferredSymmetricKeyAlgorithms( + signature: PGPSignature + ): Set = + getPreferredSymmetricAlgorithms(signature) + ?.preferences + ?.map { SymmetricKeyAlgorithm.fromId(it) } + ?.filterNotNull() + ?.toSet() + ?: setOf() + + /** + * Return the hash algorithm preferences from the signatures hashed area. + * + * @param signature signature + * @return hash algo prefs + */ + @JvmStatic + fun getPreferredHashAlgorithms(signature: PGPSignature): PreferredAlgorithms? = + hashed(signature, SignatureSubpacket.preferredHashAlgorithms) + + /** + * Return the preferred [HashAlgorithms][HashAlgorithm] as present in the signature. If no + * preference is given with regard to hash algorithms, return an empty set. + * + * In any case, the resulting set is ordered by occurrence. + * + * @param signature signature + * @return ordered set of hash algorithm preferences + */ + @JvmStatic + fun parsePreferredHashAlgorithms(signature: PGPSignature): Set = + getPreferredHashAlgorithms(signature) + ?.preferences + ?.map { HashAlgorithm.fromId(it) } + ?.filterNotNull() + ?.toSet() + ?: setOf() + + /** + * Return the compression algorithm preferences from the signatures hashed area. + * + * @param signature signature + * @return compression algo prefs + */ + @JvmStatic + fun getPreferredCompressionAlgorithms(signature: PGPSignature): PreferredAlgorithms? = + hashed(signature, SignatureSubpacket.preferredCompressionAlgorithms) + + /** + * Return the preferred [CompressionAlgorithms][CompressionAlgorithm] as present in the + * signature. If no preference is given with regard to compression algorithms, return an + * empty set. + * + * In any case, the resulting set is ordered by occurrence. + * + * @param signature signature + * @return ordered set of compression algorithm preferences + */ + @JvmStatic + fun parsePreferredCompressionAlgorithms( + signature: PGPSignature + ): Set = + getPreferredCompressionAlgorithms(signature) + ?.preferences + ?.map { CompressionAlgorithm.fromId(it) } + ?.filterNotNull() + ?.toSet() + ?: setOf() + + @JvmStatic + fun getPreferredAeadAlgorithms(signature: PGPSignature): PreferredAEADCiphersuites? = + hashed(signature, SignatureSubpacket.preferredAEADAlgorithms) + + /** + * Return the primary user-id subpacket from the signatures hashed area. + * + * @param signature signature + * @return primary user id + */ + @JvmStatic + fun getPrimaryUserId(signature: PGPSignature): PrimaryUserID? = + hashed(signature, SignatureSubpacket.primaryUserId) + + /** + * Return the key flags subpacket from the signatures hashed area. + * + * @param signature signature + * @return key flags + */ + @JvmStatic + fun getKeyFlags(signature: PGPSignature): KeyFlags? = + hashed(signature, SignatureSubpacket.keyFlags) + + /** + * Return a list of key flags carried by the signature. If the signature is null, or has no + * [KeyFlags] subpacket, return null. + * + * @param signature signature + * @return list of key flags + */ + @JvmStatic + fun parseKeyFlags(signature: PGPSignature?): List? = + signature?.let { sig -> getKeyFlags(sig)?.let { KeyFlag.fromBitmask(it.flags) } } + + /** + * Return the features subpacket from the signatures hashed area. + * + * @param signature signature + * @return features subpacket + */ + @JvmStatic + fun getFeatures(signature: PGPSignature): Features? = + hashed(signature, SignatureSubpacket.features) + + /** + * Parse out the features subpacket of a signature. If the signature has no features + * subpacket, return null. Otherwise, return the features as a feature set. + * + * @param signature signature + * @return features as set + */ + @JvmStatic + fun parseFeatures(signature: PGPSignature): Set? = + getFeatures(signature)?.let { Feature.fromBitmask(it.features.toInt()).toSet() } + + /** + * Return the signature target subpacket from the signature. We search for this subpacket in + * the hashed and unhashed area (in this order). + * + * @param signature signature + * @return signature target + */ + @JvmStatic + fun getSignatureTarget(signature: PGPSignature): SignatureTarget? = + hashedOrUnhashed(signature, SignatureSubpacket.signatureTarget) + + /** + * Return the notation data subpackets from the signatures hashed area. + * + * @param signature signature + * @return hashed notations + */ + @JvmStatic + fun getHashedNotationData(signature: PGPSignature): List = + signature.hashedSubPackets.notationDataOccurrences.toList() + + /** + * Return a list of all [NotationData] objects from the hashed area of the signature that + * have a notation name equal to the given notationName argument. + * + * @param signature signature + * @param notationName notation name + * @return list of matching notation data objects + */ + @JvmStatic + fun getHashedNotationData( + signature: PGPSignature, + notationName: String + ): List = + getHashedNotationData(signature).filter { it.notationName == notationName } + + /** + * Return the notation data subpackets from the signatures unhashed area. + * + * @param signature signature + * @return unhashed notations + */ + @JvmStatic + fun getUnhashedNotationData(signature: PGPSignature): List = + signature.unhashedSubPackets.notationDataOccurrences.toList() + + /** + * Return a list of all [NotationData] objects from the unhashed area of the signature that + * have a notation name equal to the given notationName argument. + * + * @param signature signature + * @param notationName notation name + * @return list of matching notation data objects + */ + @JvmStatic + fun getUnhashedNotationData(signature: PGPSignature, notationName: String) = + getUnhashedNotationData(signature).filter { it.notationName == notationName } + + /** + * Return the revocation key subpacket from the signatures hashed area. + * + * @param signature signature + * @return revocation key + */ + @JvmStatic + fun getRevocationKey(signature: PGPSignature): RevocationKey? = + hashed(signature, SignatureSubpacket.revocationKey) + + /** + * Return the signers user-id from the hashed area of the signature. + * + * @param signature signature + * @return signers user-id + * + * TODO: Can this subpacket also be found in the unhashed area? + */ + @JvmStatic + fun getSignerUserID(signature: PGPSignature): SignerUserID? = + hashed(signature, SignatureSubpacket.signerUserId) + + /** + * Return the intended recipients fingerprint subpackets from the hashed area of this + * signature. + * + * @param signature signature + * @return intended recipient fingerprint subpackets + */ + @JvmStatic + fun getIntendedRecipientFingerprints( + signature: PGPSignature + ): List = + signature.hashedSubPackets.intendedRecipientFingerprints.toList() + + /** + * Return the embedded signature subpacket from the signatures hashed area or unhashed area. + * + * @param signature signature + * @return embedded signature + */ + @JvmStatic + fun getEmbeddedSignature(signature: PGPSignature): PGPSignatureList = + signature.hashedSubPackets.embeddedSignatures.let { + if (it.isEmpty) signature.unhashedSubPackets.embeddedSignatures else it + } + + /** + * Return the signatures exportable certification subpacket from the hashed area. + * + * @param signature signature + * @return exportable certification subpacket + */ + @JvmStatic + fun getExportableCertification(signature: PGPSignature): Exportable? = + hashed(signature, SignatureSubpacket.exportableCertification) + + /** Return true, if the signature is not explicitly marked as non-exportable. */ + @JvmStatic + fun isExportable(signature: PGPSignature): Boolean = + getExportableCertification(signature)?.isExportable ?: true + + /** + * Return the trust signature packet from the signatures hashed area. + * + * @param signature signature + * @return trust signature subpacket + */ + @JvmStatic + fun getTrustSignature(signature: PGPSignature): TrustSignature? = + hashed(signature, SignatureSubpacket.trustSignature) + + /** + * Return the trust depth set in the signatures [TrustSignature] packet, or [defaultDepth] + * if no such packet is found. + * + * @param signature signature + * @param defaultDepth default value that is returned if no trust signature packet is found + * @return depth or default depth + */ + @JvmStatic + fun getTrustDepthOr(signature: PGPSignature, defaultDepth: Int): Int = + getTrustSignature(signature)?.depth ?: defaultDepth + + /** + * Return the trust amount set in the signatures [TrustSignature] packet, or [defaultAmount] + * if no such packet is found. + * + * @param signature signature + * @param defaultAmount default value that is returned if no trust signature packet is found + * @return amount or default amount + */ + @JvmStatic + fun getTrustAmountOr(signature: PGPSignature, defaultAmount: Int): Int = + getTrustSignature(signature)?.trustAmount ?: defaultAmount + + /** + * Return all regular expression subpackets from the hashed area of the given signature. + * + * @param signature signature + * @return list of regular expressions + */ + @JvmStatic + fun getRegularExpressions(signature: PGPSignature): List = + signature.hashedSubPackets.regularExpressions.toList() + + /** + * Select a list of all signature subpackets of the given type, which are present in either + * the hashed or the unhashed area of the given signature. + * + * @param signature signature + * @param type subpacket type + * @param

generic subpacket type + * @return list of subpackets from the hashed/unhashed area + */ + @JvmStatic + fun

hashedOrUnhashed( + signature: PGPSignature, + type: SignatureSubpacket + ): P? { + return hashed(signature, type) ?: unhashed(signature, type) + } + + /** + * Select a list of all signature subpackets of the given type, which are present in the + * hashed area of the given signature. + * + * @param signature signature + * @param type subpacket type + * @param

generic subpacket type + * @return list of subpackets from the hashed area + */ + @JvmStatic + fun

hashed( + signature: PGPSignature, + type: SignatureSubpacket + ): P? { + return getSignatureSubpacket(signature.hashedSubPackets, type) + } + + /** + * Select a list of all signature subpackets of the given type, which are present in the + * unhashed area of the given signature. + * + * @param signature signature + * @param type subpacket type + * @param

generic subpacket type + * @return list of subpackets from the unhashed area + */ + @JvmStatic + fun

unhashed( + signature: PGPSignature, + type: SignatureSubpacket + ): P? { + return getSignatureSubpacket(signature.unhashedSubPackets, type) + } + + /** + * Return the last occurrence of a subpacket type in the given signature subpacket vector. + * + * @param vector subpacket vector (hashed/unhashed) + * @param type subpacket type + * @param

generic return type of the subpacket + * @return last occurrence of the subpacket in the vector + */ + @JvmStatic + fun

getSignatureSubpacket( + vector: PGPSignatureSubpacketVector?, + type: SignatureSubpacket + ): P? { + val allPackets = vector?.getSubpackets(type.code) ?: return null + return if (allPackets.isEmpty()) null + else @Suppress("UNCHECKED_CAST") allPackets.last() as P + } + + @JvmStatic + fun assureKeyCanCarryFlags(type: KeyType, vararg flags: KeyFlag) { + assureKeyCanCarryFlags(type.algorithm, *flags) + } + + @JvmStatic + fun assureKeyCanCarryFlags(algorithm: PublicKeyAlgorithm, vararg flags: KeyFlag) { + val mask = toBitmask(*flags) + + if (!algorithm.isSigningCapable() && hasKeyFlag(mask, KeyFlag.CERTIFY_OTHER)) { + throw IllegalArgumentException( + "Algorithm $algorithm cannot be used with key flag CERTIFY_OTHER.") + } + + if (!algorithm.isSigningCapable() && hasKeyFlag(mask, KeyFlag.SIGN_DATA)) { + throw IllegalArgumentException( + "Algorithm $algorithm cannot be used with key flag SIGN_DATA.") + } + + if (!algorithm.isEncryptionCapable() && hasKeyFlag(mask, KeyFlag.ENCRYPT_COMMS)) { + throw IllegalArgumentException( + "Algorithm $algorithm cannot be used with key flag ENCRYPT_COMMS.") + } + + if (!algorithm.isEncryptionCapable() && hasKeyFlag(mask, KeyFlag.ENCRYPT_STORAGE)) { + throw IllegalArgumentException( + "Algorithm $algorithm cannot be used with key flag ENCRYPT_STORAGE.") + } + + if (!algorithm.isSigningCapable() && hasKeyFlag(mask, KeyFlag.AUTHENTICATION)) { + throw IllegalArgumentException( + "Algorithm $algorithm cannot be used with key flag AUTHENTICATION.") + } + } + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmorUtils.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmorUtils.kt new file mode 100644 index 00000000..db1cb54d --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmorUtils.kt @@ -0,0 +1,434 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.util + +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream +import org.bouncycastle.bcpg.ArmoredInputStream +import org.bouncycastle.bcpg.ArmoredOutputStream +import org.bouncycastle.openpgp.PGPKeyRing +import org.bouncycastle.openpgp.PGPPublicKey +import org.bouncycastle.openpgp.PGPPublicKeyRing +import org.bouncycastle.openpgp.PGPPublicKeyRingCollection +import org.bouncycastle.openpgp.PGPSecretKey +import org.bouncycastle.openpgp.PGPSecretKeyRing +import org.bouncycastle.openpgp.PGPSecretKeyRingCollection +import org.bouncycastle.openpgp.PGPSignature +import org.bouncycastle.openpgp.PGPUtil +import org.bouncycastle.util.io.Streams +import org.pgpainless.algorithm.HashAlgorithm +import org.pgpainless.decryption_verification.OpenPgpInputStream +import org.pgpainless.key.OpenPgpFingerprint +import org.pgpainless.key.util.KeyRingUtils + +class ArmorUtils { + + companion object { + // MessageIDs are 32 printable characters + private val PATTER_MESSAGE_ID = "^\\S{32}$".toRegex() + /** Constant armor key for comments. */ + const val HEADER_COMMENT = "Comment" + /** Constant armor key for program versions. */ + const val HEADER_VERSION = "Version" + /** Constant armor key for message IDs. Useful for split messages. */ + const val HEADER_MESSAGEID = "MessageID" + /** Constant armor key for used hash algorithms in clearsigned messages. */ + const val HEADER_HASH = "Hash" + /** Constant armor key for message character sets. */ + const val HEADER_CHARSET = "Charset" + + /** + * Return the ASCII armored encoding of the given [PGPSecretKey]. + * + * @param secretKey secret key + * @return ASCII armored encoding + * @throws IOException in case of an io error + */ + @JvmStatic + @Throws(IOException::class) + fun toAsciiArmoredString(secretKey: PGPSecretKey): String = + toAsciiArmoredString(secretKey.encoded, keyToHeader(secretKey.publicKey)) + + /** + * Return the ASCII armored encoding of the given [PGPPublicKey]. + * + * @param publicKey public key + * @return ASCII armored encoding + * @throws IOException in case of an io error + */ + @JvmStatic + @Throws(IOException::class) + fun toAsciiArmoredString(publicKey: PGPPublicKey): String = + toAsciiArmoredString(publicKey.encoded, keyToHeader(publicKey)) + + /** + * Return the ASCII armored encoding of the given [PGPSecretKeyRing]. + * + * @param secretKeys secret key ring + * @return ASCII armored encoding + * @throws IOException in case of an io error + */ + @JvmStatic + @Throws(IOException::class) + fun toAsciiArmoredString(secretKeys: PGPSecretKeyRing): String = + toAsciiArmoredString(secretKeys.encoded, keyToHeader(secretKeys.publicKey)) + + /** + * Return the ASCII armored encoding of the given [PGPPublicKeyRing]. + * + * @param certificate public key ring + * @return ASCII armored encoding + * @throws IOException in case of an io error + */ + @JvmStatic + @Throws(IOException::class) + fun toAsciiArmoredString(certificate: PGPPublicKeyRing): String = + toAsciiArmoredString(certificate.encoded, keyToHeader(certificate.publicKey)) + + /** + * Return the ASCII armored encoding of the given [PGPSecretKeyRingCollection]. The encoding + * will use per-key ASCII armors protecting each [PGPSecretKeyRing] individually. Those + * armors are then concatenated with newlines in between. + * + * @param secretKeysCollection secret key ring collection + * @return ASCII armored encoding + * @throws IOException in case of an io error + */ + @JvmStatic + @Throws(IOException::class) + fun toAsciiArmoredString(secretKeysCollection: PGPSecretKeyRingCollection): String = + secretKeysCollection.keyRings.asSequence().joinToString("\n") { + toAsciiArmoredString(it) + } + + /** + * Return the ASCII armored encoding of the given [PGPPublicKeyRingCollection]. The encoding + * will use per-key ASCII armors protecting each [PGPPublicKeyRing] individually. Those + * armors are then concatenated with newlines in between. + * + * @param certificates public key ring collection + * @return ascii armored encoding + * @throws IOException in case of an io error + */ + @JvmStatic + @Throws(IOException::class) + fun toAsciiArmoredString(certificates: PGPPublicKeyRingCollection): String = + certificates.joinToString("\n") { toAsciiArmoredString(it) } + + /** + * Return the ASCII armored representation of the given detached signature. If [export] is + * true, the signature will be stripped of non-exportable subpackets or trust-packets. If it + * is false, the signature will be encoded as-is. + * + * @param signature signature + * @param export whether to exclude non-exportable subpackets or trust-packets. + * @return ascii armored string + * @throws IOException in case of an error in the [ArmoredOutputStream] + */ + @JvmStatic + @JvmOverloads + @Throws(IOException::class) + fun toAsciiArmoredString(signature: PGPSignature, export: Boolean = false): String = + toAsciiArmoredString(signature.getEncoded(export)) + + /** + * Return the ASCII armored encoding of the given OpenPGP data bytes. The ASCII armor will + * include headers from the header map. + * + * @param bytes OpenPGP data + * @param header header map + * @return ASCII armored encoding + * @throws IOException in case of an io error + */ + @JvmStatic + @JvmOverloads + @Throws(IOException::class) + fun toAsciiArmoredString( + bytes: ByteArray, + header: Map>? = null + ): String = toAsciiArmoredString(bytes.inputStream(), header) + + /** + * Return the ASCII armored encoding of the OpenPGP data from the given [InputStream]. The + * ASCII armor will include armor headers from the given header map. + * + * @param inputStream input stream of OpenPGP data + * @param header ASCII armor header map + * @return ASCII armored encoding + * @throws IOException in case of an io error + */ + @JvmStatic + @JvmOverloads + @Throws(IOException::class) + fun toAsciiArmoredString( + inputStream: InputStream, + header: Map>? = null + ): String = + ByteArrayOutputStream() + .apply { + toAsciiArmoredStream(this, header).run { + Streams.pipeAll(inputStream, this) + this.close() + } + } + .toString() + + /** + * Return an [ArmoredOutputStream] prepared with headers for the given key ring, which wraps + * the given [OutputStream]. + * + * The armored output stream can be used to encode the key ring by calling + * [PGPKeyRing.encode] with the armored output stream as an argument. + * + * @param keys OpenPGP key or certificate + * @param outputStream wrapped output stream + * @return armored output stream + */ + @JvmStatic + @Throws(IOException::class) + fun toAsciiArmoredStream( + keys: PGPKeyRing, + outputStream: OutputStream + ): ArmoredOutputStream = toAsciiArmoredStream(outputStream, keyToHeader(keys.publicKey)) + + /** + * Create an [ArmoredOutputStream] wrapping the given [OutputStream]. The armored output + * stream will be prepared with armor headers given by header. + * + * Note: Since the armored output stream is retrieved from [ArmoredOutputStreamFactory.get], + * it may already come with custom headers. Hence, the header entries given by header are + * appended below those already populated headers. + * + * @param outputStream output stream to wrap + * @param header map of header entries + * @return armored output stream + */ + @JvmStatic + @JvmOverloads + @Throws(IOException::class) + fun toAsciiArmoredStream( + outputStream: OutputStream, + header: Map>? = null + ): ArmoredOutputStream = + ArmoredOutputStreamFactory.get(outputStream).apply { + header?.forEach { entry -> + entry.value.forEach { value -> addHeader(entry.key, value) } + } + } + + /** + * Generate a header map for ASCII armor from the given [PGPPublicKey]. The header map + * consists of a comment field of the keys pretty-printed fingerprint, as well as the + * primary or first user-id plus the count of remaining user-ids. + * + * @param publicKey public key + * @return header map + */ + @JvmStatic + private fun keyToHeader(publicKey: PGPPublicKey): Map> { + val headerMap = mutableMapOf>() + val userIds = KeyRingUtils.getUserIdsIgnoringInvalidUTF8(publicKey) + val first: String? = userIds.firstOrNull() + val primary: String? = + userIds.firstOrNull { + publicKey.getSignaturesForID(it)?.asSequence()?.any { sig -> + sig.hashedSubPackets.isPrimaryUserID + } + ?: false + } + + // Fingerprint + headerMap + .getOrPut(HEADER_COMMENT) { mutableSetOf() } + .add(OpenPgpFingerprint.of(publicKey).prettyPrint()) + // Primary / First User ID + (primary ?: first)?.let { + headerMap + .getOrPut(HEADER_COMMENT) { mutableSetOf() } + .add(it.replace("\n", "\\n").replace("\r", "\\r")) + } + // X-1 further identities + when (userIds.size) { + 0, + 1 -> {} + 2 -> headerMap.getOrPut(HEADER_COMMENT) { mutableSetOf() }.add("1 further identity") + else -> + headerMap + .getOrPut(HEADER_COMMENT) { mutableSetOf() } + .add("${userIds.size - 1} further identities") + } + return headerMap + } + + /** + * Set the version header entry in the ASCII armor. If the version info is null or only + * contains whitespace characters, then the version header will be removed. + * + * @param armor armored output stream + * @param version version header. + */ + @JvmStatic + @Deprecated( + "Changing ASCII armor headers after ArmoredOutputStream creation is deprecated. " + + "Use ArmoredOutputStream builder instead.") + fun setVersionHeader(armor: ArmoredOutputStream, version: String?) = + armor.setHeader(HEADER_VERSION, version?.let { it.ifBlank { null } }) + + /** + * Add an ASCII armor header entry about the used hash algorithm into the + * [ArmoredOutputStream]. + * + * @param armor armored output stream + * @param hashAlgorithm hash algorithm + * @see RFC 4880 - + * OpenPGP Message Format §6.2. Forming ASCII Armor + */ + @JvmStatic + @Deprecated( + "Changing ASCII armor headers after ArmoredOutputStream creation is deprecated. " + + "Use ArmoredOutputStream builder instead.") + fun addHashAlgorithmHeader(armor: ArmoredOutputStream, hashAlgorithm: HashAlgorithm) = + armor.addHeader(HEADER_HASH, hashAlgorithm.algorithmName) + + /** + * Add an ASCII armor comment header entry into the [ArmoredOutputStream]. + * + * @param armor armored output stream + * @param comment free-text comment + * @see RFC 4880 - + * OpenPGP Message Format §6.2. Forming ASCII Armor + */ + @JvmStatic + @Deprecated( + "Changing ASCII armor headers after ArmoredOutputStream creation is deprecated. " + + "Use ArmoredOutputStream builder instead.") + fun addCommentHeader(armor: ArmoredOutputStream, comment: String) = + armor.addHeader(HEADER_COMMENT, comment) + + /** + * Add an ASCII armor message-id header entry into the [ArmoredOutputStream]. + * + * @param armor armored output stream + * @param messageId message id + * @see RFC 4880 - + * OpenPGP Message Format §6.2. Forming ASCII Armor + */ + @JvmStatic + @Deprecated( + "Changing ASCII armor headers after ArmoredOutputStream creation is deprecated. " + + "Use ArmoredOutputStream builder instead.") + fun addMessageIdHeader(armor: ArmoredOutputStream, messageId: String) { + require(PATTER_MESSAGE_ID.matches(messageId)) { + "MessageIDs MUST consist of 32 printable characters." + } + armor.addHeader(HEADER_MESSAGEID, messageId) + } + + /** + * Extract all ASCII armor header values of type comment from the given + * [ArmoredInputStream]. + * + * @param armor armored input stream + * @return list of comment headers + */ + @JvmStatic + fun getCommentHeaderValues(armor: ArmoredInputStream): List = + getArmorHeaderValues(armor, HEADER_COMMENT) + + /** + * Extract all ASCII armor header values of type message id from the given + * [ArmoredInputStream]. + * + * @param armor armored input stream + * @return list of message-id headers + */ + @JvmStatic + fun getMessageIdHeaderValues(armor: ArmoredInputStream): List = + getArmorHeaderValues(armor, HEADER_MESSAGEID) + + /** + * Return all ASCII armor header values of type hash-algorithm from the given + * [ArmoredInputStream]. + * + * @param armor armored input stream + * @return list of hash headers + */ + @JvmStatic + fun getHashHeaderValues(armor: ArmoredInputStream): List = + getArmorHeaderValues(armor, HEADER_HASH) + + /** + * Return a list of [HashAlgorithm] enums extracted from the hash header entries of the + * given [ArmoredInputStream]. + * + * @param armor armored input stream + * @return list of hash algorithms from the ASCII header + */ + @JvmStatic + fun getHashAlgorithms(armor: ArmoredInputStream): List = + getHashHeaderValues(armor).mapNotNull { HashAlgorithm.fromName(it) } + + /** + * Return all ASCII armor header values of type version from the given [ArmoredInputStream]. + * + * @param armor armored input stream + * @return list of version headers + */ + @JvmStatic + fun getVersionHeaderValues(armor: ArmoredInputStream): List = + getArmorHeaderValues(armor, HEADER_VERSION) + + /** + * Return all ASCII armor header values of type charset from the given [ArmoredInputStream]. + * + * @param armor armored input stream + * @return list of charset headers + */ + @JvmStatic + fun getCharsetHeaderValues(armor: ArmoredInputStream): List = + getArmorHeaderValues(armor, HEADER_CHARSET) + + /** + * Return all ASCII armor header values of the given headerKey from the given + * [ArmoredInputStream]. + * + * @param armor armored input stream + * @param key ASCII armor header key + * @return list of values for the header key + */ + @JvmStatic + fun getArmorHeaderValues(armor: ArmoredInputStream, key: String): List = + armor.armorHeaders + .filter { it.startsWith("$key: ") } + .map { it.substring(key.length + 2) } // key.len + ": ".len + + /** + * Hacky workaround for #96. For `PGPPublicKeyRingCollection(InputStream, + * KeyFingerPrintCalculator)` or `PGPSecretKeyRingCollection(InputStream, + * KeyFingerPrintCalculator)` to read all PGPKeyRings properly, we apparently have to make + * sure that the [InputStream] that is given as constructor argument is a + * [PGPUtil.BufferedInputStreamExt]. Since [PGPUtil.getDecoderStream] will return an + * [org.bouncycastle.bcpg.ArmoredInputStream] if the underlying input stream contains + * armored data, we first dearmor the data ourselves to make sure that the end-result is a + * [PGPUtil.BufferedInputStreamExt]. + * + * @param inputStream input stream + * @return BufferedInputStreamExt + * @throws IOException in case of an IO error + */ + @JvmStatic + @Throws(IOException::class) + fun getDecoderStream(inputStream: InputStream): InputStream = + OpenPgpInputStream(inputStream).let { + if (it.isAsciiArmored) { + PGPUtil.getDecoderStream(ArmoredInputStreamFactory.get(it)) + } else { + it + } + } + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmoredInputStreamFactory.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmoredInputStreamFactory.kt new file mode 100644 index 00000000..b1956cf6 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmoredInputStreamFactory.kt @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.util + +import java.io.IOException +import java.io.InputStream +import org.bouncycastle.bcpg.ArmoredInputStream +import org.pgpainless.decryption_verification.ConsumerOptions + +/** + * Factory class for instantiating preconfigured [ArmoredInputStream] instances. [get] will return + * an [ArmoredInputStream] that is set up to properly detect CRC errors v4 style. + */ +class ArmoredInputStreamFactory { + + companion object { + + /** + * Return an instance of [ArmoredInputStream] which will detect CRC errors. + * + * @param inputStream input stream + * @return armored input stream + * @throws IOException in case of an IO error + */ + @JvmStatic + @JvmOverloads + @Throws(IOException::class) + fun get(inputStream: InputStream, options: ConsumerOptions? = null): ArmoredInputStream { + return when (inputStream) { + is ArmoredInputStream -> inputStream + else -> + ArmoredInputStream.builder() + .apply { + setParseForHeaders(true) + options?.let { setIgnoreCRC(it.isDisableAsciiArmorCRC) } + } + .build(inputStream) + } + } + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmoredOutputStreamFactory.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmoredOutputStreamFactory.kt new file mode 100644 index 00000000..caf14e53 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmoredOutputStreamFactory.kt @@ -0,0 +1,105 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.util + +import java.io.OutputStream +import org.bouncycastle.bcpg.ArmoredOutputStream +import org.pgpainless.encryption_signing.ProducerOptions + +/** + * Factory to create configured [ArmoredOutputStream] instances. The configuration entails setting + * custom version and comment headers. + */ +class ArmoredOutputStreamFactory { + + companion object { + private const val PGPAINLESS = "PGPainless" + + @JvmStatic private var version: String? = PGPAINLESS + private var comment: String? = null + + /** + * Return an instance of the [ArmoredOutputStream] which might have pre-populated armor + * headers. + * + * @param outputStream output stream + * @param options options + * @return armored output stream + */ + @JvmStatic + @JvmOverloads + fun get(outputStream: OutputStream, options: ProducerOptions? = null): ArmoredOutputStream { + val builder = + ArmoredOutputStream.builder().apply { + // set fields defined in ArmoredOutputStreamFactory + if (!version.isNullOrBlank()) setVersion(version) + if (!comment.isNullOrBlank()) setComment(comment) + + // set (and potentially overwrite with) values from ProducerOptions + options?.let { + enableCRC(!it.isDisableAsciiArmorCRC) + if (it.isHideArmorHeaders) clearHeaders() + if (it.hasVersion()) setVersion(it.version) + if (it.hasComment()) addComment(it.comment) + // TODO: configure CRC + } + } + return get(outputStream, builder) + } + + /** + * Build an [ArmoredOutputStream] around the given [outputStream], configured according to + * the passed in [ArmoredOutputStream.Builder] instance. + * + * @param outputStream output stream + * @param builder builder instance + */ + @JvmStatic + fun get( + outputStream: OutputStream, + builder: ArmoredOutputStream.Builder + ): ArmoredOutputStream { + return builder.build(outputStream) + } + + /** + * Overwrite the version header of ASCII armors with a custom value. Newlines in the version + * info string result in multiple version header entries. If this is set to

null
, + * then the version header is omitted altogether. + * + * @param versionString version string + */ + @JvmStatic + fun setVersionInfo(versionString: String?) { + version = if (versionString.isNullOrBlank()) null else versionString.trim() + } + + /** Reset the version header to its default value of [PGPAINLESS]. */ + @JvmStatic + fun resetVersionInfo() { + version = PGPAINLESS + } + + /** + * Set a comment header value in the ASCII armor header. If the comment contains newlines, + * it will be split into multiple header entries. + * + * @param commentString comment + * @see [ProducerOptions.setComment] for how to set comments for individual messages. + */ + @JvmStatic + fun setComment(commentString: String) { + require(commentString.isNotBlank()) { + "Comment cannot be empty. See resetComment() to clear the comment." + } + comment = commentString.trim() + } + + @JvmStatic + fun resetComment() { + comment = null + } + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/util/DateUtil.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/util/DateUtil.kt new file mode 100644 index 00000000..712ac262 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/util/DateUtil.kt @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.util + +import java.util.* +import openpgp.formatUTC +import openpgp.parseUTC +import openpgp.toSecondsPrecision + +class DateUtil { + + companion object { + + /** + * Parse a UTC timestamp into a date. + * + * @param dateString timestamp + * @return date + */ + @JvmStatic fun parseUTCDate(dateString: String): Date = dateString.parseUTC() + + /** + * Format a date as UTC timestamp. + * + * @param date date + * @return timestamp + */ + @JvmStatic fun formatUTCDate(date: Date): String = date.formatUTC() + + /** + * Floor a date down to seconds precision. + * + * @param date date + * @return floored date + */ + @JvmStatic fun toSecondsPrecision(date: Date): Date = date.toSecondsPrecision() + + /** + * Return the current date "floored" to UTC precision. + * + * @return now + */ + @JvmStatic fun now() = toSecondsPrecision(Date()) + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/util/MultiMap.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/util/MultiMap.kt new file mode 100644 index 00000000..3aa22d0d --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/util/MultiMap.kt @@ -0,0 +1,88 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.util + +class MultiMap : Iterable>> { + + private val map: Map> + + constructor() : this(mutableMapOf()) + + constructor(other: MultiMap) : this(other.map) + + constructor(content: Map>) { + map = mutableMapOf() + content.forEach { map[it.key] = it.value.toMutableSet() } + } + + override fun iterator(): Iterator>> { + return map.iterator() + } + + val size: Int + get() = map.size + + fun size() = size + + val keys: Set + get() = map.keys + + fun keySet() = keys + + val values: Collection> + get() = map.values + + fun values() = values + + val entries: Set>> + get() = map.entries + + fun entrySet() = entries + + fun isEmpty(): Boolean = map.isEmpty() + + fun containsKey(key: K): Boolean = map.containsKey(key) + + fun containsValue(value: V): Boolean = map.values.any { it.contains(value) } + + fun contains(key: K, value: V): Boolean = map[key]?.contains(value) ?: false + + operator fun get(key: K): Set? = map[key] + + fun put(key: K, value: V) = (map as MutableMap).put(key, mutableSetOf(value)) + + fun plus(key: K, value: V) = (map as MutableMap).getOrPut(key) { mutableSetOf() }.add(value) + + fun put(key: K, values: Set) = (map as MutableMap).put(key, values.toMutableSet()) + + fun plus(key: K, values: Set) = + (map as MutableMap).getOrPut(key) { mutableSetOf() }.addAll(values) + + fun putAll(other: MultiMap) = other.map.entries.forEach { put(it.key, it.value) } + + fun plusAll(other: MultiMap) = other.map.entries.forEach { plus(it.key, it.value) } + + fun removeAll(key: K) = (map as MutableMap).remove(key) + + fun remove(key: K, value: V) = (map as MutableMap)[key]?.remove(value) + + fun clear() = (map as MutableMap).clear() + + fun flatten() = map.flatMap { it.value }.toSet() + + override fun equals(other: Any?): Boolean { + return if (other == null) false + else if (other !is MultiMap<*, *>) false + else if (this === other) { + true + } else { + map == other.map + } + } + + override fun hashCode(): Int { + return map.hashCode() + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/util/NotationRegistry.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/util/NotationRegistry.kt new file mode 100644 index 00000000..96200b30 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/util/NotationRegistry.kt @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.util + +/** + * Registry for known notations. Since signature verification must reject signatures with critical + * notations that are not known to the application, there must be some way to tell PGPainless which + * notations actually are known. + * + * To add a notation name, call [addKnownNotation]. + */ +class NotationRegistry constructor(notations: Set = setOf()) { + private val knownNotations: MutableSet + + init { + knownNotations = notations.toMutableSet() + } + + /** + * Add a known notation name into the registry. This will cause critical notations with that + * name to no longer invalidate the signature. + * + * @param notationName name of the notation + */ + fun addKnownNotation(notationName: String): NotationRegistry = apply { + knownNotations.add(notationName) + } + + /** + * Return true if the notation name is registered in the registry. + * + * @param notationName name of the notation + * @return true if notation is known, false otherwise. + */ + fun isKnownNotation(notationName: String): Boolean = knownNotations.contains(notationName) + + /** Clear all known notations from the registry. */ + fun clear() { + knownNotations.clear() + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/util/NullOutputStream.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/util/NullOutputStream.kt new file mode 100644 index 00000000..5a3df702 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/util/NullOutputStream.kt @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.util + +import java.io.OutputStream + +/** [OutputStream] that simply discards bytes written to it. */ +class NullOutputStream : OutputStream() { + + override fun write(p0: Int) { + // nop + } + + override fun write(b: ByteArray) { + // nop + } + + override fun write(b: ByteArray, off: Int, len: Int) { + // nop + } + + override fun close() { + // nop + } + + override fun flush() { + // nop + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/util/Passphrase.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/util/Passphrase.kt new file mode 100644 index 00000000..bd25f2b9 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/util/Passphrase.kt @@ -0,0 +1,102 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.util + +import org.bouncycastle.util.Arrays + +/** + * Passphrase for keys or messages. + * + * @param chars may be null for empty passwords. + */ +class Passphrase(private val chars: CharArray?) { + private val lock = Any() + private var valid = true + + /** + * Return a copy of the underlying char array. A return value of null represents an empty + * password. + * + * @return passphrase chars. + * @throws IllegalStateException in case the password has been cleared at this point. + */ + fun getChars(): CharArray? = + synchronized(lock) { + check(valid) { "Passphrase has been cleared." } + chars?.copyOf() + } + + /** + * Return true if the passphrase has not yet been cleared. + * + * @return valid + */ + val isValid: Boolean + get() = synchronized(lock) { valid } + + /** + * Return true if the passphrase represents no password. + * + * @return empty + */ + val isEmpty: Boolean + get() = synchronized(lock) { valid && chars == null } + + /** Overwrite the char array with spaces and mark the [Passphrase] as invalidated. */ + fun clear() = + synchronized(lock) { + chars?.fill(' ') + valid = false + } + + override fun equals(other: Any?): Boolean { + return if (other == null) false + else if (this === other) true + else if (other !is Passphrase) false + else + getChars() == null && other.getChars() == null || + Arrays.constantTimeAreEqual(getChars(), other.getChars()) + } + + override fun hashCode(): Int = getChars()?.let { String(it) }.hashCode() + + /** + * Return a copy of this [Passphrase], but with whitespace characters trimmed off. + * + * @return copy with trimmed whitespace + */ + fun withTrimmedWhitespace(): Passphrase = Passphrase(trimWhitespace(chars)) + + companion object { + + /** + * Create a [Passphrase] from a [CharSequence]. + * + * @param password password + * @return passphrase + */ + @JvmStatic + fun fromPassword(password: CharSequence) = Passphrase(password.toString().toCharArray()) + + @JvmStatic fun emptyPassphrase() = Passphrase(null) + + /** + * Return a copy of the passed in char array, with leading and trailing whitespace + * characters removed. If the passed in char array is null, return null. If the resulting + * char array is empty, return null as well. + * + * @param chars char array + * @return copy of char array with leading and trailing whitespace characters removed + */ + @JvmStatic + private fun trimWhitespace(chars: CharArray?): CharArray? { + return chars + ?.dropWhile { it.isWhitespace() } + ?.dropLastWhile { it.isWhitespace() } + ?.toCharArray() + ?.let { if (it.isEmpty()) null else it } + } + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/util/SessionKey.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/util/SessionKey.kt new file mode 100644 index 00000000..ef8eb9e7 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/util/SessionKey.kt @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.util + +import org.bouncycastle.openpgp.PGPSessionKey +import org.bouncycastle.util.encoders.Hex +import org.pgpainless.algorithm.SymmetricKeyAlgorithm + +/** + * A [SessionKey] is the symmetric key that is used to encrypt/decrypt an OpenPGP message payload. + * The OpenPGP message header contains a copy of the session key, encrypted for the public key of + * each recipient. + * + * @param algorithm symmetric key algorithm + * @param key bytes of the key + */ +data class SessionKey(val algorithm: SymmetricKeyAlgorithm, val key: ByteArray) { + + /** + * Constructor to create a session key from a BC [PGPSessionKey] object. + * + * @param sessionKey BC session key + */ + constructor( + sessionKey: PGPSessionKey + ) : this(SymmetricKeyAlgorithm.requireFromId(sessionKey.algorithm), sessionKey.key) + + override fun toString(): String { + return "${algorithm.algorithmId}:${Hex.toHexString(key)}" + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as SessionKey + + if (algorithm != other.algorithm) return false + if (!key.contentEquals(other.key)) return false + + return true + } + + override fun hashCode(): Int { + return 31 * algorithm.hashCode() + key.contentHashCode() + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/util/selection/userid/SelectUserId.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/util/selection/userid/SelectUserId.kt new file mode 100644 index 00000000..f2794925 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/util/selection/userid/SelectUserId.kt @@ -0,0 +1,101 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.util.selection.userid + +import java.util.function.Predicate +import org.bouncycastle.openpgp.PGPKeyRing +import org.pgpainless.PGPainless + +abstract class SelectUserId : Predicate, (String) -> Boolean { + + /** Legacy glue code to forward accept() calls to invoke() instead. */ + @Deprecated("Use invoke() instead.", ReplaceWith("invoke(userId)")) + protected fun accept(userId: String): Boolean = invoke(userId) + + override fun test(userId: String): Boolean = invoke(userId) + + companion object { + + /** + * Filter for user-ids which match the given [query] exactly. + * + * @param query query + * @return filter + */ + @JvmStatic + fun exactMatch(query: CharSequence) = + object : SelectUserId() { + override fun invoke(userId: String): Boolean = userId == query + } + + /** + * Filter for user-ids which start with the given [substring]. + * + * @param substring substring + * @return filter + */ + @JvmStatic + fun startsWith(substring: CharSequence) = + object : SelectUserId() { + override fun invoke(userId: String): Boolean = userId.startsWith(substring) + } + + /** + * Filter for user-ids which contain the given [substring]. + * + * @param substring query + * @return filter + */ + @JvmStatic + fun containsSubstring(substring: CharSequence) = + object : SelectUserId() { + override fun invoke(userId: String): Boolean = userId.contains(substring) + } + + /** + * Filter for user-ids which contain the given [email] address. Note: This only accepts + * user-ids which properly have the email address surrounded by angle brackets. + * + * The argument [email] can both be a plain email address (`foo@bar.baz`), or surrounded by + * angle brackets (``), the result of the filter will be the same. + * + * @param email email address + * @return filter + */ + @JvmStatic + fun containsEmailAddress(email: CharSequence) = + if (email.startsWith('<') && email.endsWith('>')) containsSubstring(email) + else containsSubstring("<$email>") + + @JvmStatic + fun byEmail(email: CharSequence) = or(exactMatch(email), containsEmailAddress(email)) + + @JvmStatic + fun validUserId(keyRing: PGPKeyRing) = + object : SelectUserId() { + private val info = PGPainless.inspectKeyRing(keyRing) + + override fun invoke(userId: String): Boolean = info.isUserIdValid(userId) + } + + @JvmStatic + fun and(vararg filters: SelectUserId) = + object : SelectUserId() { + override fun invoke(userId: String): Boolean = filters.all { it.invoke(userId) } + } + + @JvmStatic + fun or(vararg filters: SelectUserId) = + object : SelectUserId() { + override fun invoke(userId: String): Boolean = filters.any { it.invoke(userId) } + } + + @JvmStatic + fun not(filter: SelectUserId) = + object : SelectUserId() { + override fun invoke(userId: String): Boolean = !filter.invoke(userId) + } + } +} diff --git a/pgpainless-core/src/main/resources/logback.xml b/pgpainless-core/src/main/resources/logback.xml deleted file mode 100644 index 13cccf62..00000000 --- a/pgpainless-core/src/main/resources/logback.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - System.err - - %blue(%-5level) %green(%logger{35}) - %msg %n - - - - - - - \ No newline at end of file diff --git a/pgpainless-core/src/test/java/investigations/InvestigateMultiSEIPMessageHandlingTest.java b/pgpainless-core/src/test/java/investigations/InvestigateMultiSEIPMessageHandlingTest.java index 3e0f083b..a0ea747a 100644 --- a/pgpainless-core/src/test/java/investigations/InvestigateMultiSEIPMessageHandlingTest.java +++ b/pgpainless-core/src/test/java/investigations/InvestigateMultiSEIPMessageHandlingTest.java @@ -4,8 +4,7 @@ package investigations; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -32,9 +31,8 @@ import org.pgpainless.algorithm.SignatureType; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.decryption_verification.ConsumerOptions; import org.pgpainless.decryption_verification.DecryptionStream; -import org.pgpainless.decryption_verification.OpenPgpMetadata; +import org.pgpainless.exception.MalformedOpenPgpMessageException; import org.pgpainless.implementation.ImplementationFactory; -import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.UnlockSecretKey; import org.pgpainless.util.Passphrase; @@ -122,7 +120,7 @@ public class InvestigateMultiSEIPMessageHandlingTest { public void generateTestMessage() throws PGPException, IOException { PGPSecretKeyRing ring1 = PGPainless.readKeyRing().secretKeyRing(KEY1); KeyRingInfo info1 = PGPainless.inspectKeyRing(ring1); - PGPPublicKey cryptKey1 = info1.getEncryptionSubkeys(EncryptionPurpose.STORAGE_AND_COMMUNICATIONS).get(0); + PGPPublicKey cryptKey1 = info1.getEncryptionSubkeys(EncryptionPurpose.ANY).get(0); PGPSecretKey signKey1 = ring1.getSecretKey(info1.getSigningSubkeys().get(0).getKeyID()); PGPSecretKeyRing ring2 = PGPainless.readKeyRing().secretKeyRing(KEY2); KeyRingInfo info2 = PGPainless.inspectKeyRing(ring2); @@ -177,7 +175,7 @@ public class InvestigateMultiSEIPMessageHandlingTest { } @Test - public void testDecryptAndVerifyDoesIgnoreAppendedSEIPData() throws IOException, PGPException { + public void testDecryptAndVerifyDetectsAppendedSEIPData() throws IOException, PGPException { PGPSecretKeyRing ring1 = PGPainless.readKeyRing().secretKeyRing(KEY1); PGPSecretKeyRing ring2 = PGPainless.readKeyRing().secretKeyRing(KEY2); @@ -191,15 +189,6 @@ public class InvestigateMultiSEIPMessageHandlingTest { .withOptions(options); ByteArrayOutputStream out = new ByteArrayOutputStream(); - Streams.pipeAll(decryptionStream, out); - decryptionStream.close(); - - assertArrayEquals(data1.getBytes(StandardCharsets.UTF_8), out.toByteArray()); - OpenPgpMetadata metadata = decryptionStream.getResult(); - assertEquals(1, metadata.getVerifiedSignatures().size(), - "The first SEIP packet is signed exactly only by the signing key of ring1."); - assertEquals( - new SubkeyIdentifier(ring1, new KeyRingInfo(ring1).getSigningSubkeys().get(0).getKeyID()), - metadata.getVerifiedSignatures().keySet().iterator().next()); + assertThrows(MalformedOpenPgpMessageException.class, () -> Streams.pipeAll(decryptionStream, out)); } } diff --git a/pgpainless-core/src/test/java/investigations/InvestigateThunderbirdDecryption.java b/pgpainless-core/src/test/java/investigations/InvestigateThunderbirdDecryption.java deleted file mode 100644 index d96f7f48..00000000 --- a/pgpainless-core/src/test/java/investigations/InvestigateThunderbirdDecryption.java +++ /dev/null @@ -1,257 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package investigations; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.Date; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.util.io.Streams; -import org.junit.jupiter.api.Test; -import org.pgpainless.PGPainless; -import org.pgpainless.algorithm.DocumentSignatureType; -import org.pgpainless.decryption_verification.ConsumerOptions; -import org.pgpainless.decryption_verification.DecryptionStream; -import org.pgpainless.encryption_signing.EncryptionOptions; -import org.pgpainless.encryption_signing.EncryptionStream; -import org.pgpainless.encryption_signing.ProducerOptions; -import org.pgpainless.encryption_signing.SigningOptions; -import org.pgpainless.key.protection.SecretKeyRingProtector; -import org.pgpainless.util.ArmorUtils; - -public class InvestigateThunderbirdDecryption { - - String OUR_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + - "Version: PGPainless\n" + - "Comment: 47D2 3A5E 1455 1FD2 0599 C1FC B57B 5451 9E2D 8FE4\n" + - "Comment: Alice \n" + - "\n" + - "lFgEYP8FlBYJKwYBBAHaRw8BAQdAeJ7fL4TbpSLUJsxGUFnN5MzDZr3lKoKWEO+z\n" + - "hQEFPqcAAP0T8ED8kcch++7UpcN7qZMP4ihbE9Fu9kp/IKOCZDVwGhF+tBxBbGlj\n" + - "ZSA8YWxpY2VAcGdwYWlubGVzcy5vcmc+iHgEExYKACAFAmD/BZQCGwEFFgIDAQAE\n" + - "CwkIBwUVCgkICwIeAQIZAQAKCRC1e1RRni2P5PYqAQC/r4R4RFfVIOPAc16PiffO\n" + - "GDMzRUYAjIyflvOBIEE//QEAsZGQzIstdIp8gY5CF27pbnnSAA/OGPXbDsNArzPN\n" + - "tQicXQRg/wWUEgorBgEEAZdVAQUBAQdAFHEP5NzgON0usvHOsTsROojwVTAqgayc\n" + - "fdPdb597u3UDAQgHAAD/ShtbTmAyZJDjcEDfUNblOogyWntCEgb18Cs5rRm1+agP\n" + - "mIh1BBgWCgAdBQJg/wWUAhsMBRYCAwEABAsJCAcFFQoJCAsCHgEACgkQtXtUUZ4t\n" + - "j+SWdwD/cCXm/ufcaIMMOqRw10Lwefc4euOrpFScWA0rUjnK6yEBAMOH1kGHlLbz\n" + - "mk6D7RbBDdC3aW4xGRjSYBkyhbuxevsDnFgEYP8FlBYJKwYBBAHaRw8BAQdAmuvN\n" + - "FF+pklSxw3+VVqVu2g2ulpJE7HldtU/Jud/jiEgAAP0RPh7QWqm2hhY6vBNr8fhz\n" + - "3GBAfZ4A9HxVymuu1M6qMxEdiNUEGBYKAH0FAmD/BZQCGwIFFgIDAQAECwkIBwUV\n" + - "CgkICwIeAV8gBBkWCgAGBQJg/wWUAAoJEIYvdZaRbR0mBesA/2dxyf9vfRnyrNcm\n" + - "dguMzYe9oLfD2SU2Sa0jXcURQ+A6AP9uYaehPZvEH0kwdeSi60uCOVznCePrY1mK\n" + - "M6UEDMPGBwAKCRC1e1RRni2P5J1FAQDhI3tN5C/klh2j8ptQ7ht0LPlbgVU/WmT8\n" + - "kqejd80WVgEA4dg7MZTk+uzwOWEGIHyxWXRzma9a5k1kM+uxX3RflQU=\n" + - "=IEzi\n" + - "-----END PGP PRIVATE KEY BLOCK-----"; - - String THEIR_CERT = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + - "Version: FlowCrypt [BUILD_REPLACEABLE_VERSION] Gmail Encryption\n" + - "Comment: Seamlessly send and receive encrypted email\n" + - "\n" + - "xjMEYL/CRRYJKwYBBAHaRw8BAQdAxaJrnD/gWRGqaAVtQ8R9PI0ZGu/YESJ4\n" + - "HsJeeCxUZOvNF0RlbiA8ZGVuQGZsb3djcnlwdC5jb20+wn4EExYKACYFAmC/\n" + - "wkUCGwMFFgIDAQAECwkIBwUVCgkICwIeAQIZAQWJAQ/XOQAKCRCGwF2G4DXc\n" + - "cttHAP9Axna+jmFhZEajILW7BZ8UJpgz7mCC48RMtRj/pre4nQD/bKJXB+sD\n" + - "zti+tRbi7KNncgkSQeau+Vy/ZnpBUUHBWwjOOARgv8JFEgorBgEEAZdVAQUB\n" + - "AQdA3dN8Hh18Pqd6OevXWl36y7cM58ZRmUVEEZukXRIholYDAQgHwnUEGBYK\n" + - "AB0FAmC/wkUCGwwFFgIDAQAECwkIBwUVCgkICwIeAQAKCRCGwF2G4DXcclpK\n" + - "AQC0uUHWUFNao1Fl85+4c8WecGKsGCihNU9H3q+I1gz22gEAtVo1dWnc0t1f\n" + - "h1MUYq5FmME+KeFCBZZ9lrMAxRhvigI=\n" + - "=+XVJ\n" + - "-----END PGP PUBLIC KEY BLOCK-----\n"; - - @Test - public void generateMessage() throws PGPException, IOException { - // CHECKSTYLE:OFF - System.out.println("Decryption Key"); - System.out.println(OUR_KEY); - // CHECKSTYLE:ON - - PGPSecretKeyRing ourKey = PGPainless.readKeyRing().secretKeyRing(OUR_KEY); - PGPPublicKeyRing ourCert = PGPainless.extractCertificate(ourKey); - PGPPublicKeyRing theirCert = PGPainless.readKeyRing().publicKeyRing(THEIR_CERT); - - // CHECKSTYLE:OFF - System.out.println("Certificate:"); - System.out.println(ArmorUtils.toAsciiArmoredString(ourCert)); - - System.out.println("Crypt-Only:"); - // CHECKSTYLE:ON - ProducerOptions producerOptions = ProducerOptions - .encrypt(new EncryptionOptions().addRecipient(ourCert).addRecipient(theirCert)) - .setFileName("msg.txt") - .setModificationDate(new Date()); - - generateMessage(producerOptions); - - // CHECKSTYLE:OFF - System.out.println("Sign-Crypt:"); - // CHECKSTYLE:ON - - producerOptions = ProducerOptions - .signAndEncrypt(new EncryptionOptions().addRecipient(ourCert).addRecipient(theirCert), - new SigningOptions().addInlineSignature(SecretKeyRingProtector.unprotectedKeys(), ourKey, DocumentSignatureType.BINARY_DOCUMENT)) - .setFileName("msg.txt") - .setModificationDate(new Date()); - - generateMessage(producerOptions); - } - - private void generateMessage(ProducerOptions producerOptions) throws PGPException, IOException { - String data = "Hello World\n"; - ByteArrayOutputStream out = new ByteArrayOutputStream(); - EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() - .onOutputStream(out) - .withOptions(producerOptions); - - Streams.pipeAll(new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8)), encryptionStream); - encryptionStream.close(); - - // CHECKSTYLE:OFF - System.out.println(out); - // CHECKSTYLE:ON - } - - @Test - public void testWithSuspiciousKey() throws PGPException, IOException { - String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + - "Comment: 4409 A346 1359 96C9 4595 510E 5A18 53D1 5656 CB7A\n" + - "Comment: Alice (Created with pgpkeygen.com) +// +// SPDX-License-Identifier: Apache-2.0 + +package investigations; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.exception.KeyIntegrityException; +import org.pgpainless.key.generation.type.rsa.RsaLength; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.key.protection.UnlockSecretKey; +import org.pgpainless.key.util.KeyIdUtil; +import org.pgpainless.util.Passphrase; + +public class ModifiedPublicKeysInvestigation { + + private static final String DSA = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: OpenPGP.js VERSION\n" + + "Comment: https://openpgpjs.org\n" + + "\n" + + "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\n" + + "=p9kX\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + + private static final String ELGAMAL = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: OpenPGP.js VERSION\n" + + "Comment: https://openpgpjs.org\n" + + "\n" + + "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\n" + + "TsMBqN9H5d+2XQ==\n" + + "=lI+G\n" + + "-----END PGP PRIVATE KEY BLOCK-----\n"; + + // created with exploit code by cure53.de + private static final String INJECTED_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: F594 D7CC E7D0 1F15 1511 4395 C075 FD34 4B2A D41A\n" + + "Comment: Juliet \n" + + "\n" + + "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\n" + + "=WEYQ\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + + @Test + public void assertModifiedDSAKeyThrowsKeyIntegrityException() throws IOException { + SecretKeyRingProtector protector = SecretKeyRingProtector.unlockAnyKeyWith(Passphrase.fromPassword("12345678")); + PGPSecretKeyRing dsa = PGPainless.readKeyRing().secretKeyRing(DSA); + + PGPainless.getPolicy().setEnableKeyParameterValidation(true); + assertThrows(KeyIntegrityException.class, () -> + UnlockSecretKey.unlockSecretKey(dsa.getSecretKey(KeyIdUtil.fromLongKeyId("b1bd1f049ec87f3d")), protector)); + assertThrows(KeyIntegrityException.class, () -> + UnlockSecretKey.unlockSecretKey(dsa.getSecretKey(KeyIdUtil.fromLongKeyId("f5ffdf6d71dd5789")), protector)); + PGPainless.getPolicy().setEnableKeyParameterValidation(false); + } + + @Test + public void assertModifiedElGamalKeyThrowsKeyIntegrityException() throws IOException { + SecretKeyRingProtector protector = SecretKeyRingProtector.unlockAnyKeyWith(Passphrase.fromPassword("12345678")); + PGPSecretKeyRing elgamal = PGPainless.readKeyRing().secretKeyRing(ELGAMAL); + + PGPainless.getPolicy().setEnableKeyParameterValidation(true); + assertThrows(KeyIntegrityException.class, () -> + UnlockSecretKey.unlockSecretKey(elgamal.getSecretKey(KeyIdUtil.fromLongKeyId("f5ffdf6d71dd5789")), protector)); + PGPainless.getPolicy().setEnableKeyParameterValidation(false); + } + + @Test + public void assertInjectedKeyRingFailsToUnlockPrimaryKey() throws IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(INJECTED_KEY); + SecretKeyRingProtector protector = SecretKeyRingProtector.unlockAnyKeyWith(Passphrase.fromPassword("pass")); + + PGPainless.getPolicy().setEnableKeyParameterValidation(true); + assertThrows(KeyIntegrityException.class, () -> + UnlockSecretKey.unlockSecretKey(secretKeys.getSecretKey(), protector)); + PGPainless.getPolicy().setEnableKeyParameterValidation(false); + } + + @Test + public void assertCannotUnlockElGamalPrimaryKeyDueToDummyS2K() throws IOException { + SecretKeyRingProtector protector = SecretKeyRingProtector.unlockAnyKeyWith(Passphrase.fromPassword("12345678")); + PGPSecretKeyRing elgamal = PGPainless.readKeyRing().secretKeyRing(ELGAMAL); + + assertThrows(PGPException.class, () -> + UnlockSecretKey.unlockSecretKey(elgamal.getSecretKey(KeyIdUtil.fromLongKeyId("5f04acf44fd822b1")), protector)); + } + + @Test + public void assertUnmodifiedRSAKeyDoesNotThrow() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() + .simpleRsaKeyRing("Unmodified", RsaLength._4096, "987654321"); + SecretKeyRingProtector protector = SecretKeyRingProtector.unlockAnyKeyWith(Passphrase.fromPassword("987654321")); + + for (PGPSecretKey secretKey : secretKeys) { + assertDoesNotThrow(() -> + UnlockSecretKey.unlockSecretKey(secretKey, protector)); + } + } + + @Test + public void assertUnmodifiedECKeyDoesNotThrow() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() + .simpleEcKeyRing("Unmodified", "987654321"); + SecretKeyRingProtector protector = SecretKeyRingProtector.unlockAnyKeyWith(Passphrase.fromPassword("987654321")); + + for (PGPSecretKey secretKey : secretKeys) { + assertDoesNotThrow(() -> + UnlockSecretKey.unlockSecretKey(secretKey, protector)); + } + } + + @Test + public void assertUnmodifiedModernKeyDoesNotThrow() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() + .modernKeyRing("Unmodified", "987654321"); + SecretKeyRingProtector protector = SecretKeyRingProtector.unlockAnyKeyWith(Passphrase.fromPassword("987654321")); + + for (PGPSecretKey secretKey : secretKeys) { + assertDoesNotThrow(() -> + UnlockSecretKey.unlockSecretKey(secretKey, protector)); + } + } +} diff --git a/pgpainless-core/src/test/java/investigations/OnePassSignatureVerificationWithPartialLengthLiteralDataRegressionTest.java b/pgpainless-core/src/test/java/investigations/OnePassSignatureVerificationWithPartialLengthLiteralDataRegressionTest.java new file mode 100644 index 00000000..7ec53edb --- /dev/null +++ b/pgpainless-core/src/test/java/investigations/OnePassSignatureVerificationWithPartialLengthLiteralDataRegressionTest.java @@ -0,0 +1,135 @@ +// SPDX-FileCopyrightText: 2021 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package investigations; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import org.bouncycastle.bcpg.ArmoredInputStream; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.decryption_verification.ConsumerOptions; +import org.pgpainless.decryption_verification.DecryptionStream; + +public class OnePassSignatureVerificationWithPartialLengthLiteralDataRegressionTest { + + /** + * Signed and Encrypted Message created with PGPainless 0.2.9. + * PGPainless versions 0.2.10 - 0.2.18 fail to decrypt this message, due to it failing to parse the signatures trailing + * the literal data. The cause for this was not draining the literal data first before trying to parse the sigs. + * This is likely caused by the literal data using a partial length encoding scheme, so the PGPObjectFactory did not yet + * reach the signatures packets. + * + * As a fix, PGPainless now only tries to parse signatures from after the literal data packet, once the literal data + * stream gets closed. + */ + public static final String MSG = "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "aEY0RHRHcWVYOENCUGRzU0FRZEFVTjJrSkZNb2lJUHhCUEFSWkZodnJxU2FGd090\n" + + "c3llR2pkU1l4bS9UdFJRd01JK09PUGJYVjlnUEM3VEZFemlKWmRmL0ZxcUVaQTNV\n" + + "ZkhIeEo3Y0hnWlhQWWw1Q29LMU5aSW9NRC9udk1iT1poRjREYmtNdFV2TkpWL3dT\n" + + "QVFkQW00b01SQXVVbTdYL1BZUTc3T3Q2ZUxwTWs2VDk3TmhHMzB6RFFDSUMvV1l3\n" + + "TTUvZkR4dW1uVW5ucXNwVFVJSmhRMmVYM0I2R2NtVE5ZdXVmSUNIbGZKMU9UQk05\n" + + "MklNMkVGWHU1M2x3TVBLYTB1a0IzRWltbmJRNUpCNTBpT2NUeDZCcDJQREJZK0VN\n" + + "K29IdDVlUzFzOWxlZjJUNHdCY0w1ejFLU3hQTkRpODh6Skp6dTZ1b3BxMXFwdWVI\n" + + "UDFtemYzN0NTY3lJTHpJK0lwRXVUbWwvODdyK294TWVQR3NvR3NwblBuUWFXa0xY\n" + + "dzdGVHpnWUJ5SGxyS3gzTGJIT040bDFVbC90dnhMbFBwNE5aRmJQcjQwWlYxb0o4\n" + + "eE9JczRTaXpZSTNDUGRXQmlNVXJiaDJRMEFBTkg4aWNyMjhDeUZneDFSenpGdFRZ\n" + + "MzVjeE5HSXRRZzRoR3BNUmVOWDdWNHpWOFRsUkFJSEVtaFRCTHpGZXR4eWJCbFJh\n" + + "c3l0SUN0eWVydnZiNTQ3V2htK2tDWUxRQUcyOUlwZXUxOWo2MnV1dHJjWm10YWJn\n" + + "LzEyTG5HSEczRkxoMGxHTmNOZnd3OXN6VC9zV0RXM2swQ3RCdVpsSmFUVXFLYlY2\n" + + "QkRsTjZMWXFvYi9ad01wcDE4WGVuTk5tU2ZsL2JpcHZ0UE1hMk5NdGVuWXV2SGVO\n" + + "R2hZK3Q0MFE3NE5OYmJRV1dsVXFqakFYZ3NOaUhsTjhDV2Z3UG82Ykx1OW9PaEFL\n" + + "eTgvbFlNL1dlL2hlUFFpVGpqUUVaM3J2OHVDVGdCekFuc2tqazd0bUVOdTdnclJz\n" + + "WjBSdzlYelRwTzJlTCtHRmV3VlhOMzNWUzFHVnR5QTMyVFRCd1ZDcStaNEtCMXRX\n" + + "MVFIRUlDekc2UldsMkR5djBmZENpc2FoQU5SLzBmQXRrZm0wU3k1R1htWm5pWU9L\n" + + "MkhiN2NZeHEzREs1MHowWTN4WkdiemE4L2VUMzlPTG1jMG5DdWQ5cktHaUkya0Er\n" + + "a0NDQzF5UUlrek9zZDZlU1pFR1FncFV5UHlxdDRNQUhYeDcxUkFuR0NiWW9OVkRY\n" + + "aUQwZ0d0M2lZRHFJV1N0TGErek1xbkJWN085Z3lSZFFVN2lXR25CeW9QNnlXc1Nk\n" + + "aVBRSW5RR3RVSFZabU0wQnBwUk45ZUo0QlVJd2RvY0lIRldjZ0xNQjNiYlBDWHVF\n" + + "bGl6N1ZPSHBFVWVYVmNWNWl6Z3NVUEJOSVVOZWxHcElrSk5Xa0lSSndMSFVnUlR0\n" + + "SEh1ZFMyNnJZeURoU0tGcjdiM01HdWwyVU9GdTFlM0FzK24yVkJjcGN0ZHFtTGxG\n" + + "THU3ZGxHMGJ0dHJQVWhaYyt4NjlFenUraTRtamRoZzZyVC9ydnYvRTJmRTRUVlpN\n" + + "MGExbk5CUG40UT09\n" + + "=mKyE\n" + + "-----END PGP MESSAGE-----"; + + public static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 23A2 3010 2038 66BC B390 8598 BB0C CFD4 57D4 DE77\n" + + "Comment: xmpp:one@exampletwo.org\n" + + "\n" + + "lFgEYXQMCRYJKwYBBAHaRw8BAQdA1NhQdMUKkiwSI92ETqlY2lrAt4EbehgzpWMs\n" + + "sm1Ke34AAP4sx3S3r0qoNpGyi3o7zfet60xIIkw9qKNdnYQyvouFhRFftBd4bXBw\n" + + "Om9uZUBleGFtcGxldHdvLm9yZ4h4BBMWCgAgBQJhdAwJAhsBBRYCAwEABRUKCQgL\n" + + "BAsJCAcCHgECGQEACgkQuwzP1FfU3ncAWQD/dUR7rbOpV8H4CTIpDJXiDuWi1vkC\n" + + "Rmm5jFQsJlrIzZEA/0aZSEXH3Gj5OdQGy9qKrvqGkq7idjrTkh3gYiWRB+EOnF0E\n" + + "YXQMCRIKKwYBBAGXVQEFAQEHQCobua4HJAsmfCB9TFjBSRfP1FEIEht4MMl4rHN4\n" + + "eWc0AwEIBwAA/0Tmh56XX8bVDof1VVCdapcCC+LAA3wSH5SfP+EVaIJoD8WIdQQY\n" + + "FgoAHQUCYXQMCQIbDAUWAgMBAAUVCgkICwQLCQgHAh4BAAoJELsMz9RX1N533dQB\n" + + "ANRojORnaZw224DRVhONAuQazhKZz3e13MhyTFi91BhmAP9chFgUkvpiorQ6I65D\n" + + "iCM315VHIvorrIElhKDtYu65CZxYBGF0DAkWCSsGAQQB2kcPAQEHQB3vy1KMKzDG\n" + + "/yooOsvfNXtdFh8ROWWth2CZAh1rt3fdAAD+KVMkDED4xf7h1/aAunFAmdZ+xGTo\n" + + "uPbTr8vWQMrVUFAUi4jVBBgWCgB9BQJhdAwJAhsCBRYCAwEABRUKCQgLBAsJCAcC\n" + + "HgFfIAQZFgoABgUCYXQMCQAKCRDFaY6lJy4mR/FEAP9dHZi975eqlSdRa5pEn1xz\n" + + "TLBfz2mAfWLQEr2kWLLVRAD+JBsyldKsUF8q1m/D/ty0lUUSGslgOhTcEoXxx3yC\n" + + "1wwACgkQuwzP1FfU3neefwEA82brBIEKARYD/zHwNEPGLZweZHLPV5Iu9dmBw3l9\n" + + "tmoA/RlQYaAKD86S1ZcfPIbjDIZkL9sjFh5tK0+mSl8rv4UH\n" + + "=/1RX\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + + public static final String CERT = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: FC0A 2CB3 F757 8B26 442C 7091 A7BA 7031 BD1E 0D5F\n" + + "Comment: xmpp:one@exampleone.org\n" + + "\n" + + "mDMEYXQMCRYJKwYBBAHaRw8BAQdA01hwFPFYUpsGGUpf21BUlwoL9tVVAnR3sS+J\n" + + "UZSUlka0F3htcHA6b25lQGV4YW1wbGVvbmUub3JniHgEExYKACAFAmF0DAkCGwEF\n" + + "FgIDAQAFFQoJCAsECwkIBwIeAQIZAQAKCRCnunAxvR4NX+f7AQCjzT+r25dDlUpp\n" + + "tocSQtgEmWZabwB41ykD/XfyBtM0RAD/ba4yYv+f/4mX7u3XpJxkrKFs/bHwyWsR\n" + + "VapeUGxhKwa4OARhdAwJEgorBgEEAZdVAQUBAQdAlbrJ+h8CygRFZBsx+Rsm4Kp+\n" + + "VCB7yUR2IxOrmiGqUlsDAQgHiHUEGBYKAB0FAmF0DAkCGwwFFgIDAQAFFQoJCAsE\n" + + "CwkIBwIeAQAKCRCnunAxvR4NX3bmAP4mTtMWgKl7RkAB/pSLMJ4bbTMSMUJCH/jS\n" + + "qz/PNtmVrgD+JLrWg2+hNPAA8zJx8LH73G4YzZMSQ0CBd9nmWRZr3w+4MwRhdAwJ\n" + + "FgkrBgEEAdpHDwEBB0BrLuiD0Xb6/N66IehUl77qh/Q0vDa8ack6TcOIwxZsHIjV\n" + + "BBgWCgB9BQJhdAwJAhsCBRYCAwEABRUKCQgLBAsJCAcCHgFfIAQZFgoABgUCYXQM\n" + + "CQAKCRD97UDyQowaGe1MAPwJeSe2vkEcMIk711lBbAsambR7D72XVyc0F8maniUy\n" + + "LwD8Dbgx8O0bCcd7fcXztfyZe8OtGKQk19fSLd+xp5VThwkACgkQp7pwMb0eDV8y\n" + + "aQEA+g10lq+1gkaLBXZbc/mUJ4odIjYBk0JdGgU8oTAZd58A/2UT9C5G9ht/lMhK\n" + + "hISFnP6CXwvy6L1XA9bjXQJ0unMF\n" + + "=OyZq\n" + + "-----END PGP PUBLIC KEY BLOCK-----"; + + @Test + public void testDecryptAndVerify_0_2_9_message() throws IOException, PGPException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); + PGPPublicKeyRing cert = PGPainless.readKeyRing().publicKeyRing(CERT); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + ByteArrayOutputStream dearmored = new ByteArrayOutputStream(); + ArmoredInputStream armorIn = new ArmoredInputStream(new ByteArrayInputStream(MSG.getBytes(StandardCharsets.UTF_8))); + Streams.pipeAll(armorIn, dearmored); + armorIn.close(); + + ByteArrayInputStream in = new ByteArrayInputStream(dearmored.toByteArray()); + + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(in) + .withOptions(new ConsumerOptions() + .addVerificationCert(cert) + .addDecryptionKey(secretKeys)); + + Streams.pipeAll(decryptionStream, out); + decryptionStream.close(); + decryptionStream.getMetadata(); + } +} diff --git a/pgpainless-core/src/test/java/investigations/RNGPerformanceTest.java b/pgpainless-core/src/test/java/investigations/RNGPerformanceTest.java new file mode 100644 index 00000000..e583089d --- /dev/null +++ b/pgpainless-core/src/test/java/investigations/RNGPerformanceTest.java @@ -0,0 +1,88 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package investigations; + +import org.bouncycastle.crypto.digests.SHA1Digest; +import org.bouncycastle.crypto.digests.SHA256Digest; +import org.bouncycastle.crypto.prng.DigestRandomGenerator; +import org.junit.jupiter.api.Test; +import org.junit.platform.commons.logging.Logger; +import org.junit.platform.commons.logging.LoggerFactory; + +import java.security.SecureRandom; +import java.time.Duration; +import java.time.Instant; +import java.util.Random; + +/** + * Evaluate performance of random number generators. + */ +public class RNGPerformanceTest { + + private static final Logger LOGGER = LoggerFactory.getLogger(RNGPerformanceTest.class); + private static final int bytesInMebiByte = 1024 * 1024; + + @Test + public void evaluateRandomPerformance() { + Random random = new Random(); + byte[] bytes = new byte[bytesInMebiByte]; + + Instant start = Instant.now(); + random.nextBytes(bytes); + Instant end = Instant.now(); + + Duration duration = Duration.between(start, end); + LOGGER.info(() -> String.format( + "Random.nextBytes() took %s milliseconds to generate 1 MiB of data", + duration.toMillis())); + } + + @Test + public void evaluateSecureRandomPerformance() { + SecureRandom random = new SecureRandom(); + byte[] bytes = new byte[bytesInMebiByte]; + + Instant start = Instant.now(); + random.nextBytes(bytes); + Instant end = Instant.now(); + + Duration duration = Duration.between(start, end); + LOGGER.info(() -> String.format( + "SecureRandom.nextBytes() took %s milliseconds to generate 1 MiB of data", + duration.toMillis())); + } + + @Test + public void evaluateSHA256BasedDigestRandomGeneratorPerformance() { + SHA256Digest digest = new SHA256Digest(); + DigestRandomGenerator random = new DigestRandomGenerator(digest); + byte[] bytes = new byte[bytesInMebiByte]; + + Instant start = Instant.now(); + random.nextBytes(bytes); + Instant end = Instant.now(); + + Duration duration = Duration.between(start, end); + LOGGER.info(() -> String.format( + "SHA256-based DigestRandomGenerator.nextBytes() took %s milliseconds to generate 1 MiB of data", + duration.toMillis())); + } + + @Test + public void evaluateSHA1BasedDigestRandomGeneratorPerformance() { + SHA1Digest digest = new SHA1Digest(); + DigestRandomGenerator random = new DigestRandomGenerator(digest); + byte[] bytes = new byte[bytesInMebiByte]; + + Instant start = Instant.now(); + random.nextBytes(bytes); + Instant end = Instant.now(); + + Duration duration = Duration.between(start, end); + LOGGER.info(() -> String.format( + "SHA1-based DigestRandomGenerator.nextBytes() took %s milliseconds to generate 1 MiB of data", + duration.toMillis())); + } +} diff --git a/pgpainless-core/src/test/java/investigations/TimeTest.java b/pgpainless-core/src/test/java/investigations/TimeTest.java new file mode 100644 index 00000000..0807b161 --- /dev/null +++ b/pgpainless-core/src/test/java/investigations/TimeTest.java @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: 2018 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package investigations; + +import org.junit.jupiter.api.Test; + +import java.util.Date; + +import static org.junit.JUtils.assertEquals; + +/** + * Exploratory test for date and time related operations. + */ +public class TimeTest { + + @Test + public void newDateGetTimeEqualsSystemCurrentTimeMillis() { + Date date = new Date(); + long dateTime = date.getTime(); + long currentTime = System.currentTimeMillis(); + + assertEquals(dateTime, currentTime, 10); + } +} diff --git a/pgpainless-core/src/test/java/org/bouncycastle/ArmoredInputStreamOverArmoredDataWithAppendedCleartextTest.java b/pgpainless-core/src/test/java/org/bouncycastle/ArmoredInputStreamOverArmoredDataWithAppendedCleartextTest.java new file mode 100644 index 00000000..06921294 --- /dev/null +++ b/pgpainless-core/src/test/java/org/bouncycastle/ArmoredInputStreamOverArmoredDataWithAppendedCleartextTest.java @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.bouncycastle; + +import org.bouncycastle.bcpg.ArmoredInputStream; +import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class ArmoredInputStreamOverArmoredDataWithAppendedCleartextTest { + + private static final String ASCII_ARMORED_WITH_APPENDED_GARBAGE = "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "yxRiAAAAAABIZWxsbywgV29ybGQhCg==\n" + + "=WGju\n" + + "-----END PGP MESSAGE-----\n" + + "This is a bunch of crap that we appended."; + @Test + public void testArmoredInputStreamCutsOffAnyDataAfterTheAsciiArmor() throws IOException { + InputStream inputStream = new ByteArrayInputStream(ASCII_ARMORED_WITH_APPENDED_GARBAGE.getBytes(StandardCharsets.UTF_8)); + ArmoredInputStream armorIn = new ArmoredInputStream(inputStream); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Streams.pipeAll(armorIn, out); + armorIn.close(); + + assertEquals(22, out.size(), "ArmoredInputStream cuts off any appended data outside the ASCII armor."); + } +} diff --git a/pgpainless-core/src/test/java/org/bouncycastle/AsciiArmorCRCTests.java b/pgpainless-core/src/test/java/org/bouncycastle/AsciiArmorCRCTests.java index 19d72dda..f9bd7a61 100644 --- a/pgpainless-core/src/test/java/org/bouncycastle/AsciiArmorCRCTests.java +++ b/pgpainless-core/src/test/java/org/bouncycastle/AsciiArmorCRCTests.java @@ -278,12 +278,11 @@ public class AsciiArmorCRCTests { /** * This test verifies, whether PGPainless can read PGPSecretKeyRings from ASCII armored encodings * where the armor is missing its CRC checksum. + * * @see Sequoia Test Suite - * @throws PGPException - * @throws IOException */ @Test - public void missingCRCInArmoredKeyDoesNotCauseException() throws PGPException, IOException { + public void missingCRCInArmoredKeyDoesNotCauseException() throws IOException { String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + "Comment: Bob's OpenPGP Transferable Secret Key\n" + "\n" + @@ -490,7 +489,7 @@ public class AsciiArmorCRCTests { Passphrase passphrase = Passphrase.fromPassword("flowcrypt compatibility tests"); @Test - public void testInvalidArmorCRCThrowsOnClose() throws PGPException, IOException { + public void testInvalidArmorCRCThrowsOnClose() throws IOException { String message = "-----BEGIN PGP MESSAGE-----\n" + "Version: FlowCrypt 5.0.4 Gmail Encryption flowcrypt.com\n" + "Comment: Seamlessly send, receive and search encrypted email\n" + @@ -543,14 +542,16 @@ public class AsciiArmorCRCTests { "-----END PGP MESSAGE-----\n"; PGPSecretKeyRing key = PGPainless.readKeyRing().secretKeyRing(ASCII_KEY); - DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() - .onInputStream(new ByteArrayInputStream(message.getBytes(StandardCharsets.UTF_8))) - .withOptions(new ConsumerOptions().addDecryptionKey( - key, SecretKeyRingProtector.unlockAllKeysWith(passphrase, key) - )); + assertThrows(IOException.class, () -> { + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(new ByteArrayInputStream(message.getBytes(StandardCharsets.UTF_8))) + .withOptions(new ConsumerOptions().addDecryptionKey( + key, SecretKeyRingProtector.unlockAnyKeyWith(passphrase) + )); - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - Streams.pipeAll(decryptionStream, outputStream); - assertThrows(IOException.class, decryptionStream::close); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + Streams.pipeAll(decryptionStream, outputStream); + decryptionStream.close(); + }); } } diff --git a/pgpainless-core/src/test/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java b/pgpainless-core/src/test/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java new file mode 100644 index 00000000..10cf4b1f --- /dev/null +++ b/pgpainless-core/src/test/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java @@ -0,0 +1,101 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.bouncycastle; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPrivateKey; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.EncryptionPurpose; +import org.pgpainless.decryption_verification.CachingBcPublicKeyDataDecryptorFactory; +import org.pgpainless.decryption_verification.ConsumerOptions; +import org.pgpainless.decryption_verification.DecryptionStream; +import org.pgpainless.key.SubkeyIdentifier; +import org.pgpainless.key.info.KeyRingInfo; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.key.protection.UnlockSecretKey; + +public class CachingBcPublicKeyDataDecryptorFactoryTest { + + private static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: C8AE 4279 5958 5F46 86A9 8B5F EC69 7C29 2BE4 44E0\n" + + "Comment: Alice\n" + + "\n" + + "lFgEY1vEcxYJKwYBBAHaRw8BAQdAXOUK1uc1iBeM+mMt2nLCukXWoJd/SodrtN9S\n" + + "U/zzwu0AAP9eePPw91KLuq6PF9jQoTRz/cW4CyiALNJpsOJIZ1rp3xOBtAVBbGlj\n" + + "ZYiPBBMWCgBBBQJjW8RzCRDsaXwpK+RE4BYhBMiuQnlZWF9GhqmLX+xpfCkr5ETg\n" + + "Ap4BApsBBRYCAwEABAsJCAcFFQoJCAsCmQEAAGqWAQC8oz7l8izjUis5ji+sgI+q\n" + + "gML22VNybqmLBpzZwnNU5wEApe9fNTRbK5yAITGBscxH7o74Qe+CLI6Ni5MwzKxr\n" + + "5AucXQRjW8RzEgorBgEEAZdVAQUBAQdAm8xk0QSvpp2ZU1KQ31E7eEZYLKpbW4JE\n" + + "opmtMQx6AlIDAQgHAAD/XTb/qSosfkNvli3BQiUzVRAqKaU4PKAq7at6afxoYSgN\n" + + "4Yh1BBgWCgAdBQJjW8RzAp4BApsMBRYCAwEABAsJCAcFFQoJCAsACgkQ7Gl8KSvk\n" + + "ROB38QEA0MvDt0bjEXwFoM0E34z0MtPcG3VBYcQ+iFRIqFfEl5UA/2yZxFjoZqrs\n" + + "AQE8TaVpXYfbc2p/GEKA9LGd9l/g0QQLnFgEY1vEcxYJKwYBBAHaRw8BAQdAyCOv\n" + + "6hGUvHcCBSDKP3fRz+scyJ9zwMt7nFXK5A/k2YgAAQCn3Es+IhvePn3eBlcYMMr0\n" + + "xcktrY1NJAIZPfjlUJ0J1g6LiNUEGBYKAH0FAmNbxHMCngECmwIFFgIDAQAECwkI\n" + + "BwUVCgkIC18gBBkWCgAGBQJjW8RzAAoJECxLf7KoUc8wD18BANNpIr4E+RRVVztR\n" + + "OVwdxSe0SRWGjkW8nHrRyghHKTuMAP9p4ZKicOYA1uZbiNNjyuJuS8xBH6Hihurb\n" + + "gDypVgxdBQAKCRDsaXwpK+RE4EQjAP9ARZEPxKNLFkrvjoZ8nrts3qhv3VtMrU+9\n" + + "huZnYLe1FQEAtgO6V7wutHvVARHXqPJ6lcv+SueIu+BjLFYEKuBwggs=\n" + + "=ShJd\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + + private static final String MSG = "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "hF4DJmQMTBqw3G8SAQdALkHpO0UkS/CqkwxUz74MJU3PV72ZrIL8ZcrO8ofhblkw\n" + + "iDIhSwwGTG3tj+sG+ZVWKsmONKi7Om5seJDHQtQ8MfdCELAgwYHSt6MrgDBhuDIH\n" + + "0kABZhq2/8qk3EGXPpc+xxs4r4g8SgHOiiHSim5NGtounXXIaF6T/hUmlorkeYf/\n" + + "a9pCC0QXRUAr8NOcdsfbvb5V\n" + + "=dQa8\n" + + "-----END PGP MESSAGE-----"; + + @Test + public void test() throws PGPException, IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); + SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); + KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); + SubkeyIdentifier decryptionKey = new SubkeyIdentifier(secretKeys, + info.getEncryptionSubkeys(EncryptionPurpose.ANY).get(0).getKeyID()); + + PGPSecretKey secretKey = secretKeys.getSecretKey(decryptionKey.getSubkeyId()); + PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKey, protector); + CachingBcPublicKeyDataDecryptorFactory cachingFactory = new CachingBcPublicKeyDataDecryptorFactory( + privateKey, decryptionKey); + + ByteArrayInputStream ciphertextIn = new ByteArrayInputStream(MSG.getBytes()); + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(ciphertextIn) + .withOptions(ConsumerOptions.get() + .addCustomDecryptorFactory(cachingFactory)); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Streams.pipeAll(decryptionStream, out); + decryptionStream.close(); + assertEquals("Hello, World!\n", out.toString()); + + ciphertextIn = new ByteArrayInputStream(MSG.getBytes()); + decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(ciphertextIn) + .withOptions(ConsumerOptions.get() + .addCustomDecryptorFactory(cachingFactory)); + out = new ByteArrayOutputStream(); + Streams.pipeAll(decryptionStream, out); + decryptionStream.close(); + assertEquals("Hello, World!\n", out.toString()); + + cachingFactory.clear(); + } +} diff --git a/pgpainless-core/src/test/java/org/bouncycastle/LiteralDataCRLFEncodingTest.java b/pgpainless-core/src/test/java/org/bouncycastle/LiteralDataCRLFEncodingTest.java new file mode 100644 index 00000000..b6f92bc6 --- /dev/null +++ b/pgpainless-core/src/test/java/org/bouncycastle/LiteralDataCRLFEncodingTest.java @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: 2021 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.bouncycastle; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.Date; + +import org.bouncycastle.openpgp.PGPCanonicalizedDataGenerator; +import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.Test; + +public class LiteralDataCRLFEncodingTest { + + @Test + public void testCanonicalization() throws IOException { + PGPCanonicalizedDataGenerator generator = new PGPCanonicalizedDataGenerator(); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + OutputStream canonicalizer = generator.open(out, PGPCanonicalizedDataGenerator.UTF8, "", new Date(), new byte[1 << 9]); + + ByteArrayInputStream in = new ByteArrayInputStream("Foo\nBar\n".getBytes(StandardCharsets.UTF_8)); + Streams.pipeAll(in, canonicalizer); + canonicalizer.close(); + + byte[] bytes = out.toByteArray(); + byte[] canonicalized = new byte[bytes.length - 8]; // header is not interesting + System.arraycopy(bytes, 8, canonicalized, 0, canonicalized.length); + assertArrayEquals(new byte[] { + // F o o \r \n B a r \r \n + 70, 111, 111, 13, 10, 66, 97, 114, 13, 10}, + canonicalized); + } +} diff --git a/pgpainless-core/src/test/java/org/bouncycastle/PGPPublicKeyRingTest.java b/pgpainless-core/src/test/java/org/bouncycastle/PGPPublicKeyRingTest.java index 411e81e5..ebb344b0 100644 --- a/pgpainless-core/src/test/java/org/bouncycastle/PGPPublicKeyRingTest.java +++ b/pgpainless-core/src/test/java/org/bouncycastle/PGPPublicKeyRingTest.java @@ -28,7 +28,7 @@ public class PGPPublicKeyRingTest { * Learning test to see if BC also makes userids available on subkeys. * It does not. * - * see also https://security.stackexchange.com/questions/92635/is-it-possible-to-assign-different-uids-to-subkeys-for-the-purpose-of-having-mul + * @see +// +// SPDX-License-Identifier: Apache-2.0 + +package org.gnupg; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import org.bouncycastle.bcpg.S2K; +import org.bouncycastle.bcpg.SecretKeyPacket; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.key.SubkeyIdentifier; +import org.pgpainless.key.util.KeyIdUtil; + +public class GnuPGDummyKeyUtilTest { + // normal, non-hw-backed key + private static final String FULL_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 01FD AB6C E04A 5078 79FE 4A18 C312 C97D A9F7 6A4F\n" + + "Comment: Hardy Hardware \n" + + "\n" + + "lFgEY1vSiBYJKwYBBAHaRw8BAQdAQ58lZn/HOtg+1b1KS18odyQ6M4LaDdbJAyRf\n" + + "eBwCeTQAAPwJN+Xmr0jjN7RA9jgqXnxC/rcWHmdp/j9NdEd7K2Wbxw/rtCBIYXJk\n" + + "eSBIYXJkd2FyZSA8aGFyZHlAaGFyZC53YXJlPoiPBBMWCgBBBQJjW9KICRDDEsl9\n" + + "qfdqTxYhBAH9q2zgSlB4ef5KGMMSyX2p92pPAp4BApsBBRYCAwEABAsJCAcFFQoJ\n" + + "CAsCmQEAAPk2AP922T5TQ7hukFlpxX3ThMhieJnECGY5Eqt5U0/vEY1XdgD/eE1M\n" + + "l9qqx6QGcaNKe8deMe3EhTant6mS9tqMHp2/3gmcXQRjW9KIEgorBgEEAZdVAQUB\n" + + "AQdAVXBLNvNmFh9KX6iLmdNJM28Zc9PGnzEoAD9+T4p0lDwDAQgHAAD/fw9hnzeH\n" + + "VtBaHi6efXvnc4rdVj8zWk0LKo1clFd3bTAN+oh1BBgWCgAdBQJjW9KIAp4BApsM\n" + + "BRYCAwEABAsJCAcFFQoJCAsACgkQwxLJfan3ak/JyQD9GBj0vjtYZAf5Fi0eEKdi\n" + + "Ags0yZrQPkMs6eL+83te770A/jG0DeJy+88fOfWTj+mixO98PZPnQ0MybWC/1QUT\n" + + "vP0BnFgEY1vSiBYJKwYBBAHaRw8BAQdAvSYTD60t8vx10dSEBACUoIfVCpeOB30D\n" + + "6nfwJtbDT0YAAQCgnCsN9iX7s2TQd8NPggWs4QdhaFpb6olt3SlAvUy/wRBDiNUE\n" + + "GBYKAH0FAmNb0ogCngECmwIFFgIDAQAECwkIBwUVCgkIC18gBBkWCgAGBQJjW9KI\n" + + "AAoJEJQCL6VtwFtJDmMBAKqsGfRFQxJXyPgugWBgEaO5lt9fMM0yUxa76cmSWe5f\n" + + "AQD2oLSEW1GOgIs64+Z3gvtXopmeupT09HhI7ger98zDAwAKCRDDEsl9qfdqTwR6\n" + + "AP9Xftw8xZ7/MWhYImk/xheqPy07K4qo3T1pGKUvUqjWQQEAhE3r0oTcJn+KVCwG\n" + + "jF6AYiLOzO/R1x5bSlYD3FeJ3Qo=\n" + + "=+vXp\n" + + "-----END PGP PRIVATE KEY BLOCK-----\n"; + + private static final long primaryKeyId = KeyIdUtil.fromLongKeyId("C312C97DA9F76A4F"); + private static final long encryptionKeyId = KeyIdUtil.fromLongKeyId("6924D066714CE8C6"); + private static final long signatureKeyId = KeyIdUtil.fromLongKeyId("94022FA56DC05B49"); + private static final byte[] cardSerial = new byte[] {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f}; + + public static final String ALL_KEYS_ON_CARD = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 01FD AB6C E04A 5078 79FE 4A18 C312 C97D A9F7 6A4F\n" + + "Comment: Hardy Hardware \n" + + "\n" + + "lEwEY1vSiBYJKwYBBAHaRw8BAQdAQ58lZn/HOtg+1b1KS18odyQ6M4LaDdbJAyRf\n" + + "eBwCeTT+AGUAR05VAhAAAQIDBAUGBwgJCgsMDQ4PtCBIYXJkeSBIYXJkd2FyZSA8\n" + + "aGFyZHlAaGFyZC53YXJlPoiPBBMWCgBBBQJjW9KICRDDEsl9qfdqTxYhBAH9q2zg\n" + + "SlB4ef5KGMMSyX2p92pPAp4BApsBBRYCAwEABAsJCAcFFQoJCAsCmQEAAPk2AP92\n" + + "2T5TQ7hukFlpxX3ThMhieJnECGY5Eqt5U0/vEY1XdgD/eE1Ml9qqx6QGcaNKe8de\n" + + "Me3EhTant6mS9tqMHp2/3gmcUQRjW9KIEgorBgEEAZdVAQUBAQdAVXBLNvNmFh9K\n" + + "X6iLmdNJM28Zc9PGnzEoAD9+T4p0lDwDAQgH/gBlAEdOVQIQAAECAwQFBgcICQoL\n" + + "DA0OD4h1BBgWCgAdBQJjW9KIAp4BApsMBRYCAwEABAsJCAcFFQoJCAsACgkQwxLJ\n" + + "fan3ak/JyQD9GBj0vjtYZAf5Fi0eEKdiAgs0yZrQPkMs6eL+83te770A/jG0DeJy\n" + + "+88fOfWTj+mixO98PZPnQ0MybWC/1QUTvP0BnEwEY1vSiBYJKwYBBAHaRw8BAQdA\n" + + "vSYTD60t8vx10dSEBACUoIfVCpeOB30D6nfwJtbDT0b+AGUAR05VAhAAAQIDBAUG\n" + + "BwgJCgsMDQ4PiNUEGBYKAH0FAmNb0ogCngECmwIFFgIDAQAECwkIBwUVCgkIC18g\n" + + "BBkWCgAGBQJjW9KIAAoJEJQCL6VtwFtJDmMBAKqsGfRFQxJXyPgugWBgEaO5lt9f\n" + + "MM0yUxa76cmSWe5fAQD2oLSEW1GOgIs64+Z3gvtXopmeupT09HhI7ger98zDAwAK\n" + + "CRDDEsl9qfdqTwR6AP9Xftw8xZ7/MWhYImk/xheqPy07K4qo3T1pGKUvUqjWQQEA\n" + + "hE3r0oTcJn+KVCwGjF6AYiLOzO/R1x5bSlYD3FeJ3Qo=\n" + + "=rYoa\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + + public static final String ALL_KEYS_REMOVED = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 01FD AB6C E04A 5078 79FE 4A18 C312 C97D A9F7 6A4F\n" + + "Comment: Hardy Hardware \n" + + "\n" + + "lDsEY1vSiBYJKwYBBAHaRw8BAQdAQ58lZn/HOtg+1b1KS18odyQ6M4LaDdbJAyRf\n" + + "eBwCeTT+AGUAR05VAbQgSGFyZHkgSGFyZHdhcmUgPGhhcmR5QGhhcmQud2FyZT6I\n" + + "jwQTFgoAQQUCY1vSiAkQwxLJfan3ak8WIQQB/ats4EpQeHn+ShjDEsl9qfdqTwKe\n" + + "AQKbAQUWAgMBAAQLCQgHBRUKCQgLApkBAAD5NgD/dtk+U0O4bpBZacV904TIYniZ\n" + + "xAhmORKreVNP7xGNV3YA/3hNTJfaqsekBnGjSnvHXjHtxIU2p7epkvbajB6dv94J\n" + + "nEAEY1vSiBIKKwYBBAGXVQEFAQEHQFVwSzbzZhYfSl+oi5nTSTNvGXPTxp8xKAA/\n" + + "fk+KdJQ8AwEIB/4AZQBHTlUBiHUEGBYKAB0FAmNb0ogCngECmwwFFgIDAQAECwkI\n" + + "BwUVCgkICwAKCRDDEsl9qfdqT8nJAP0YGPS+O1hkB/kWLR4Qp2ICCzTJmtA+Qyzp\n" + + "4v7ze17vvQD+MbQN4nL7zx859ZOP6aLE73w9k+dDQzJtYL/VBRO8/QGcOwRjW9KI\n" + + "FgkrBgEEAdpHDwEBB0C9JhMPrS3y/HXR1IQEAJSgh9UKl44HfQPqd/Am1sNPRv4A\n" + + "ZQBHTlUBiNUEGBYKAH0FAmNb0ogCngECmwIFFgIDAQAECwkIBwUVCgkIC18gBBkW\n" + + "CgAGBQJjW9KIAAoJEJQCL6VtwFtJDmMBAKqsGfRFQxJXyPgugWBgEaO5lt9fMM0y\n" + + "Uxa76cmSWe5fAQD2oLSEW1GOgIs64+Z3gvtXopmeupT09HhI7ger98zDAwAKCRDD\n" + + "Esl9qfdqTwR6AP9Xftw8xZ7/MWhYImk/xheqPy07K4qo3T1pGKUvUqjWQQEAhE3r\n" + + "0oTcJn+KVCwGjF6AYiLOzO/R1x5bSlYD3FeJ3Qo=\n" + + "=GEN/\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + + public static final String PRIMARY_KEY_ON_CARD = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 01FD AB6C E04A 5078 79FE 4A18 C312 C97D A9F7 6A4F\n" + + "Comment: Hardy Hardware \n" + + "\n" + + "lEwEY1vSiBYJKwYBBAHaRw8BAQdAQ58lZn/HOtg+1b1KS18odyQ6M4LaDdbJAyRf\n" + + "eBwCeTT+AGUAR05VAhAAAQIDBAUGBwgJCgsMDQ4PtCBIYXJkeSBIYXJkd2FyZSA8\n" + + "aGFyZHlAaGFyZC53YXJlPoiPBBMWCgBBBQJjW9KICRDDEsl9qfdqTxYhBAH9q2zg\n" + + "SlB4ef5KGMMSyX2p92pPAp4BApsBBRYCAwEABAsJCAcFFQoJCAsCmQEAAPk2AP92\n" + + "2T5TQ7hukFlpxX3ThMhieJnECGY5Eqt5U0/vEY1XdgD/eE1Ml9qqx6QGcaNKe8de\n" + + "Me3EhTant6mS9tqMHp2/3gmcXQRjW9KIEgorBgEEAZdVAQUBAQdAVXBLNvNmFh9K\n" + + "X6iLmdNJM28Zc9PGnzEoAD9+T4p0lDwDAQgHAAD/fw9hnzeHVtBaHi6efXvnc4rd\n" + + "Vj8zWk0LKo1clFd3bTAN+oh1BBgWCgAdBQJjW9KIAp4BApsMBRYCAwEABAsJCAcF\n" + + "FQoJCAsACgkQwxLJfan3ak/JyQD9GBj0vjtYZAf5Fi0eEKdiAgs0yZrQPkMs6eL+\n" + + "83te770A/jG0DeJy+88fOfWTj+mixO98PZPnQ0MybWC/1QUTvP0BnFgEY1vSiBYJ\n" + + "KwYBBAHaRw8BAQdAvSYTD60t8vx10dSEBACUoIfVCpeOB30D6nfwJtbDT0YAAQCg\n" + + "nCsN9iX7s2TQd8NPggWs4QdhaFpb6olt3SlAvUy/wRBDiNUEGBYKAH0FAmNb0ogC\n" + + "ngECmwIFFgIDAQAECwkIBwUVCgkIC18gBBkWCgAGBQJjW9KIAAoJEJQCL6VtwFtJ\n" + + "DmMBAKqsGfRFQxJXyPgugWBgEaO5lt9fMM0yUxa76cmSWe5fAQD2oLSEW1GOgIs6\n" + + "4+Z3gvtXopmeupT09HhI7ger98zDAwAKCRDDEsl9qfdqTwR6AP9Xftw8xZ7/MWhY\n" + + "Imk/xheqPy07K4qo3T1pGKUvUqjWQQEAhE3r0oTcJn+KVCwGjF6AYiLOzO/R1x5b\n" + + "SlYD3FeJ3Qo=\n" + + "=zQLi\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + + public static final String ENCRYPTION_KEY_ON_CARD = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 01FD AB6C E04A 5078 79FE 4A18 C312 C97D A9F7 6A4F\n" + + "Comment: Hardy Hardware \n" + + "\n" + + "lFgEY1vSiBYJKwYBBAHaRw8BAQdAQ58lZn/HOtg+1b1KS18odyQ6M4LaDdbJAyRf\n" + + "eBwCeTQAAPwJN+Xmr0jjN7RA9jgqXnxC/rcWHmdp/j9NdEd7K2Wbxw/rtCBIYXJk\n" + + "eSBIYXJkd2FyZSA8aGFyZHlAaGFyZC53YXJlPoiPBBMWCgBBBQJjW9KICRDDEsl9\n" + + "qfdqTxYhBAH9q2zgSlB4ef5KGMMSyX2p92pPAp4BApsBBRYCAwEABAsJCAcFFQoJ\n" + + "CAsCmQEAAPk2AP922T5TQ7hukFlpxX3ThMhieJnECGY5Eqt5U0/vEY1XdgD/eE1M\n" + + "l9qqx6QGcaNKe8deMe3EhTant6mS9tqMHp2/3gmcUQRjW9KIEgorBgEEAZdVAQUB\n" + + "AQdAVXBLNvNmFh9KX6iLmdNJM28Zc9PGnzEoAD9+T4p0lDwDAQgH/gBlAEdOVQIQ\n" + + "AAECAwQFBgcICQoLDA0OD4h1BBgWCgAdBQJjW9KIAp4BApsMBRYCAwEABAsJCAcF\n" + + "FQoJCAsACgkQwxLJfan3ak/JyQD9GBj0vjtYZAf5Fi0eEKdiAgs0yZrQPkMs6eL+\n" + + "83te770A/jG0DeJy+88fOfWTj+mixO98PZPnQ0MybWC/1QUTvP0BnFgEY1vSiBYJ\n" + + "KwYBBAHaRw8BAQdAvSYTD60t8vx10dSEBACUoIfVCpeOB30D6nfwJtbDT0YAAQCg\n" + + "nCsN9iX7s2TQd8NPggWs4QdhaFpb6olt3SlAvUy/wRBDiNUEGBYKAH0FAmNb0ogC\n" + + "ngECmwIFFgIDAQAECwkIBwUVCgkIC18gBBkWCgAGBQJjW9KIAAoJEJQCL6VtwFtJ\n" + + "DmMBAKqsGfRFQxJXyPgugWBgEaO5lt9fMM0yUxa76cmSWe5fAQD2oLSEW1GOgIs6\n" + + "4+Z3gvtXopmeupT09HhI7ger98zDAwAKCRDDEsl9qfdqTwR6AP9Xftw8xZ7/MWhY\n" + + "Imk/xheqPy07K4qo3T1pGKUvUqjWQQEAhE3r0oTcJn+KVCwGjF6AYiLOzO/R1x5b\n" + + "SlYD3FeJ3Qo=\n" + + "=7OZu\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + + public static final String SIGNATURE_KEY_ON_CARD = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 01FD AB6C E04A 5078 79FE 4A18 C312 C97D A9F7 6A4F\n" + + "Comment: Hardy Hardware \n" + + "\n" + + "lFgEY1vSiBYJKwYBBAHaRw8BAQdAQ58lZn/HOtg+1b1KS18odyQ6M4LaDdbJAyRf\n" + + "eBwCeTQAAPwJN+Xmr0jjN7RA9jgqXnxC/rcWHmdp/j9NdEd7K2Wbxw/rtCBIYXJk\n" + + "eSBIYXJkd2FyZSA8aGFyZHlAaGFyZC53YXJlPoiPBBMWCgBBBQJjW9KICRDDEsl9\n" + + "qfdqTxYhBAH9q2zgSlB4ef5KGMMSyX2p92pPAp4BApsBBRYCAwEABAsJCAcFFQoJ\n" + + "CAsCmQEAAPk2AP922T5TQ7hukFlpxX3ThMhieJnECGY5Eqt5U0/vEY1XdgD/eE1M\n" + + "l9qqx6QGcaNKe8deMe3EhTant6mS9tqMHp2/3gmcXQRjW9KIEgorBgEEAZdVAQUB\n" + + "AQdAVXBLNvNmFh9KX6iLmdNJM28Zc9PGnzEoAD9+T4p0lDwDAQgHAAD/fw9hnzeH\n" + + "VtBaHi6efXvnc4rdVj8zWk0LKo1clFd3bTAN+oh1BBgWCgAdBQJjW9KIAp4BApsM\n" + + "BRYCAwEABAsJCAcFFQoJCAsACgkQwxLJfan3ak/JyQD9GBj0vjtYZAf5Fi0eEKdi\n" + + "Ags0yZrQPkMs6eL+83te770A/jG0DeJy+88fOfWTj+mixO98PZPnQ0MybWC/1QUT\n" + + "vP0BnEwEY1vSiBYJKwYBBAHaRw8BAQdAvSYTD60t8vx10dSEBACUoIfVCpeOB30D\n" + + "6nfwJtbDT0b+AGUAR05VAhAAAQIDBAUGBwgJCgsMDQ4PiNUEGBYKAH0FAmNb0ogC\n" + + "ngECmwIFFgIDAQAECwkIBwUVCgkIC18gBBkWCgAGBQJjW9KIAAoJEJQCL6VtwFtJ\n" + + "DmMBAKqsGfRFQxJXyPgugWBgEaO5lt9fMM0yUxa76cmSWe5fAQD2oLSEW1GOgIs6\n" + + "4+Z3gvtXopmeupT09HhI7ger98zDAwAKCRDDEsl9qfdqTwR6AP9Xftw8xZ7/MWhY\n" + + "Imk/xheqPy07K4qo3T1pGKUvUqjWQQEAhE3r0oTcJn+KVCwGjF6AYiLOzO/R1x5b\n" + + "SlYD3FeJ3Qo=\n" + + "=GpEw\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + + @Test + public void testMoveAllKeysToCard() throws IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(FULL_KEY); + PGPSecretKeyRing expected = PGPainless.readKeyRing().secretKeyRing(ALL_KEYS_ON_CARD); + + PGPSecretKeyRing onCard = GnuPGDummyKeyUtil.modify(secretKeys) + .divertPrivateKeysToCard(GnuPGDummyKeyUtil.KeyFilter.any(), cardSerial); + + for (PGPSecretKey key : onCard) { + assertEquals(SecretKeyPacket.USAGE_SHA1, key.getS2KUsage()); + S2K s2K = key.getS2K(); + assertEquals(S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD, s2K.getProtectionMode()); + } + + assertArrayEquals(expected.getEncoded(), onCard.getEncoded()); + } + + @Test + public void testMovePrimaryKeyToCard() throws IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(FULL_KEY); + PGPSecretKeyRing expected = PGPainless.readKeyRing().secretKeyRing(PRIMARY_KEY_ON_CARD); + + PGPSecretKeyRing onCard = GnuPGDummyKeyUtil.modify(secretKeys) + .divertPrivateKeysToCard(GnuPGDummyKeyUtil.KeyFilter.only(primaryKeyId), cardSerial); + + assertArrayEquals(expected.getEncoded(), onCard.getEncoded()); + } + + @Test + public void testMoveEncryptionKeyToCard() throws IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(FULL_KEY); + PGPSecretKeyRing expected = PGPainless.readKeyRing().secretKeyRing(ENCRYPTION_KEY_ON_CARD); + + PGPSecretKeyRing onCard = GnuPGDummyKeyUtil.modify(secretKeys) + .divertPrivateKeysToCard(GnuPGDummyKeyUtil.KeyFilter.only(encryptionKeyId), cardSerial); + + assertArrayEquals(expected.getEncoded(), onCard.getEncoded()); + } + + @Test + public void testMoveSigningKeyToCard() throws IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(FULL_KEY); + PGPSecretKeyRing expected = PGPainless.readKeyRing().secretKeyRing(SIGNATURE_KEY_ON_CARD); + + PGPSecretKeyRing onCard = GnuPGDummyKeyUtil.modify(secretKeys) + .divertPrivateKeysToCard(GnuPGDummyKeyUtil.KeyFilter.only(signatureKeyId), cardSerial); + + assertArrayEquals(expected.getEncoded(), onCard.getEncoded()); + } + + @Test + public void testRemoveAllKeys() throws IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(FULL_KEY); + PGPSecretKeyRing expected = PGPainless.readKeyRing().secretKeyRing(ALL_KEYS_REMOVED); + + PGPSecretKeyRing removedSecretKeys = GnuPGDummyKeyUtil.modify(secretKeys) + .removePrivateKeys(GnuPGDummyKeyUtil.KeyFilter.any()); + + for (PGPSecretKey key : removedSecretKeys) { + assertEquals(key.getS2KUsage(), SecretKeyPacket.USAGE_SHA1); + S2K s2k = key.getS2K(); + assertEquals(GnuPGDummyExtension.NO_PRIVATE_KEY.getId(), s2k.getProtectionMode()); + } + + assertArrayEquals(expected.getEncoded(), removedSecretKeys.getEncoded()); + } + + @Test + public void testGetSingleIdOfHardwareBackedKey() throws IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(FULL_KEY); + assertTrue(GnuPGDummyKeyUtil.getIdsOfKeysWithGnuPGS2KDivertedToCard(secretKeys).isEmpty()); + + PGPSecretKeyRing withHardwareBackedEncryptionKey = GnuPGDummyKeyUtil.modify(secretKeys) + .divertPrivateKeysToCard(GnuPGDummyKeyUtil.KeyFilter.only(encryptionKeyId)); + + Set hardwareBackedKeys = GnuPGDummyKeyUtil + .getIdsOfKeysWithGnuPGS2KDivertedToCard(withHardwareBackedEncryptionKey); + assertEquals(Collections.singleton(new SubkeyIdentifier(secretKeys, encryptionKeyId)), hardwareBackedKeys); + } + + + @Test + public void testGetIdsOfFullyHardwareBackedKey() throws IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(FULL_KEY); + assertTrue(GnuPGDummyKeyUtil.getIdsOfKeysWithGnuPGS2KDivertedToCard(secretKeys).isEmpty()); + + PGPSecretKeyRing withHardwareBackedEncryptionKey = GnuPGDummyKeyUtil.modify(secretKeys) + .divertPrivateKeysToCard(GnuPGDummyKeyUtil.KeyFilter.any()); + Set expected = new HashSet<>(); + for (PGPSecretKey key : secretKeys) { + expected.add(new SubkeyIdentifier(secretKeys, key.getKeyID())); + } + + Set hardwareBackedKeys = GnuPGDummyKeyUtil + .getIdsOfKeysWithGnuPGS2KDivertedToCard(withHardwareBackedEncryptionKey); + + assertEquals(expected, hardwareBackedKeys); + } +} diff --git a/pgpainless-core/src/test/java/org/junit/JUtils.java b/pgpainless-core/src/test/java/org/junit/JUtils.java index 31dddd01..50dc8ccc 100644 --- a/pgpainless-core/src/test/java/org/junit/JUtils.java +++ b/pgpainless-core/src/test/java/org/junit/JUtils.java @@ -4,6 +4,7 @@ package org.junit; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.Date; @@ -19,4 +20,8 @@ public class JUtils { public static void assertDateEquals(Date a, Date b) { org.junit.jupiter.api.Assertions.assertEquals(DateUtil.formatUTCDate(a), DateUtil.formatUTCDate(b)); } + + public static void assertDateNotEquals(Date a, Date b) { + assertNotEquals(DateUtil.formatUTCDate(a), DateUtil.formatUTCDate(b)); + } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/algorithm/AEADAlgorithmTest.java b/pgpainless-core/src/test/java/org/pgpainless/algorithm/AEADAlgorithmTest.java new file mode 100644 index 00000000..58423190 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/algorithm/AEADAlgorithmTest.java @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.algorithm; + +import org.junit.jupiter.api.Test; + +import java.util.NoSuchElementException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class AEADAlgorithmTest { + + @Test + public void testEAXParameters() { + AEADAlgorithm eax = AEADAlgorithm.EAX; + assertEquals(1, eax.getAlgorithmId()); + assertEquals(16, eax.getIvLength()); + assertEquals(16, eax.getTagLength()); + } + + @Test + public void testOCBParameters() { + AEADAlgorithm ocb = AEADAlgorithm.OCB; + assertEquals(2, ocb.getAlgorithmId()); + assertEquals(15, ocb.getIvLength()); + assertEquals(16, ocb.getTagLength()); + } + + @Test + public void testGCMParameters() { + AEADAlgorithm gcm = AEADAlgorithm.GCM; + assertEquals(3, gcm.getAlgorithmId()); + assertEquals(12, gcm.getIvLength()); + assertEquals(16, gcm.getTagLength()); + } + + @Test + public void testFromId() { + assertEquals(AEADAlgorithm.EAX, AEADAlgorithm.requireFromId(1)); + assertEquals(AEADAlgorithm.OCB, AEADAlgorithm.requireFromId(2)); + assertEquals(AEADAlgorithm.GCM, AEADAlgorithm.requireFromId(3)); + + assertNull(AEADAlgorithm.fromId(99)); + assertThrows(NoSuchElementException.class, () -> AEADAlgorithm.requireFromId(99)); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/algorithm/FeatureTest.java b/pgpainless-core/src/test/java/org/pgpainless/algorithm/FeatureTest.java index 347322f8..e9d0b86e 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/algorithm/FeatureTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/algorithm/FeatureTest.java @@ -6,20 +6,37 @@ package org.pgpainless.algorithm; import org.junit.jupiter.api.Test; +import java.util.NoSuchElementException; + import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; public class FeatureTest { + @Test + public void testAll() { + for (Feature feature : Feature.values()) { + assertEquals(feature, Feature.fromId(feature.getFeatureId())); + assertEquals(feature, Feature.requireFromId(feature.getFeatureId())); + } + } + @Test public void testModificationDetection() { Feature modificationDetection = Feature.MODIFICATION_DETECTION; assertEquals(0x01, modificationDetection.getFeatureId()); assertEquals(modificationDetection, Feature.fromId((byte) 0x01)); + assertEquals(modificationDetection, Feature.requireFromId((byte) 0x01)); } @Test public void testFromInvalidIdIsNull() { assertNull(Feature.fromId((byte) 0x99)); } + + @Test + public void testRequireFromInvalidThrows() { + assertThrows(NoSuchElementException.class, () -> Feature.requireFromId((byte) 0x99)); + } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/algorithm/KeyFlagTest.java b/pgpainless-core/src/test/java/org/pgpainless/algorithm/KeyFlagTest.java new file mode 100644 index 00000000..3ec5dcb6 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/algorithm/KeyFlagTest.java @@ -0,0 +1,97 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.algorithm; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +public class KeyFlagTest { + + @Test + public void testEmptyBitmaskHasNoFlags() { + int bitmask = KeyFlag.toBitmask(); + assertEquals(0, bitmask); + for (KeyFlag flag : KeyFlag.values()) { + assertFalse(KeyFlag.hasKeyFlag(bitmask, flag)); + } + } + + @Test + public void testEmptyBitmaskToKeyFlags() { + int emptyMask = 0; + List flags = KeyFlag.fromBitmask(emptyMask); + assertTrue(flags.isEmpty()); + } + + @Test + public void testSingleBitmaskToKeyFlags() { + for (KeyFlag flag : KeyFlag.values()) { + int singleMask = KeyFlag.toBitmask(flag); + List singletonList = KeyFlag.fromBitmask(singleMask); + assertEquals(1, singletonList.size()); + assertEquals(flag, singletonList.get(0)); + } + } + + @Test + public void testKeyFlagsToBitmaskToList() { + int bitMask = KeyFlag.toBitmask(KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE); + List flags = KeyFlag.fromBitmask(bitMask); + + assertEquals(2, flags.size()); + assertTrue(flags.contains(KeyFlag.ENCRYPT_COMMS)); + assertTrue(flags.contains(KeyFlag.ENCRYPT_STORAGE)); + } + + @Test + public void testSingleKeyFlagToBitmask() { + for (KeyFlag flag : KeyFlag.values()) { + int bitmask = KeyFlag.toBitmask(flag); + assertEquals(flag.getFlag(), bitmask); + } + } + + @Test + public void testDuplicateFlagsDoNotChangeMask() { + int mask = KeyFlag.toBitmask(KeyFlag.ENCRYPT_STORAGE, KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_COMMS); + assertEquals(KeyFlag.toBitmask(KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE), mask); + } + + @Test + public void testMaskHasNot() { + int mask = KeyFlag.toBitmask(KeyFlag.ENCRYPT_STORAGE); + assertFalse(KeyFlag.hasKeyFlag(mask, KeyFlag.ENCRYPT_COMMS)); + } + + @Test + public void testMaskContainsNone() { + int mask = KeyFlag.toBitmask(KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE); + + assertFalse(KeyFlag.containsAny(mask, KeyFlag.SIGN_DATA, KeyFlag.CERTIFY_OTHER)); + } + + @Test + public void testContainsAnyContainsAllExact() { + int mask = KeyFlag.toBitmask(KeyFlag.ENCRYPT_STORAGE, KeyFlag.ENCRYPT_COMMS); + assertTrue(KeyFlag.containsAny(mask, KeyFlag.ENCRYPT_STORAGE, KeyFlag.ENCRYPT_COMMS)); + } + + @Test + public void testContainsAnyContainsAll() { + int mask = KeyFlag.toBitmask(KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA, KeyFlag.AUTHENTICATION); + assertTrue(KeyFlag.containsAny(mask, KeyFlag.SIGN_DATA, KeyFlag.AUTHENTICATION)); + } + + @Test + public void testContainsAnyContainsSome() { + int mask = KeyFlag.toBitmask(KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA, KeyFlag.AUTHENTICATION); + assertTrue(KeyFlag.containsAny(mask, KeyFlag.CERTIFY_OTHER)); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/algorithm/OpenPgpPacketTest.java b/pgpainless-core/src/test/java/org/pgpainless/algorithm/OpenPgpPacketTest.java new file mode 100644 index 00000000..88146605 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/algorithm/OpenPgpPacketTest.java @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.algorithm; + +import org.bouncycastle.bcpg.PacketTags; +import org.junit.jupiter.api.Test; + +import java.util.NoSuchElementException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class OpenPgpPacketTest { + + @Test + public void testFromInvalidTag() { + int tag = PacketTags.RESERVED; + assertNull(OpenPgpPacket.fromTag(tag)); + assertThrows(NoSuchElementException.class, + () -> OpenPgpPacket.requireFromTag(tag)); + } + + @Test + public void testFromExistingTags() { + for (OpenPgpPacket p : OpenPgpPacket.values()) { + assertEquals(p, OpenPgpPacket.fromTag(p.getTag())); + assertEquals(p, OpenPgpPacket.requireFromTag(p.getTag())); + } + } + + @Test + public void testPKESKTagMatches() { + assertEquals(PacketTags.PUBLIC_KEY_ENC_SESSION, OpenPgpPacket.PKESK.getTag()); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/algorithm/RevocationStateTest.java b/pgpainless-core/src/test/java/org/pgpainless/algorithm/RevocationStateTest.java new file mode 100644 index 00000000..f5189eb9 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/algorithm/RevocationStateTest.java @@ -0,0 +1,111 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.algorithm; + +import org.junit.jupiter.api.Test; +import org.pgpainless.util.DateUtil; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.NoSuchElementException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class RevocationStateTest { + + @Test + public void softRevokedRequiresDate() { + // noinspection DataFlowIssue, ResultOfMethodCallIgnored + assertThrows(NullPointerException.class, () -> + RevocationState.softRevoked(null)); + } + + @Test + public void testNotRevoked() { + RevocationState state = RevocationState.notRevoked(); + assertEquals(RevocationStateType.notRevoked, state.getType()); + assertTrue(state.isNotRevoked()); + assertFalse(state.isHardRevocation()); + assertFalse(state.isSoftRevocation()); + assertThrows(NoSuchElementException.class, state::getDate); + assertEquals("notRevoked", state.toString()); + } + + @Test + public void testHardRevoked() { + RevocationState state = RevocationState.hardRevoked(); + assertEquals(RevocationStateType.hardRevoked, state.getType()); + assertTrue(state.isHardRevocation()); + assertFalse(state.isSoftRevocation()); + assertFalse(state.isNotRevoked()); + + assertThrows(NoSuchElementException.class, state::getDate); + assertEquals("hardRevoked", state.toString()); + } + + @Test + public void testSoftRevoked() { + Date date = DateUtil.parseUTCDate("2022-08-03 18:26:35 UTC"); + assertNotNull(date); + + RevocationState state = RevocationState.softRevoked(date); + assertEquals(RevocationStateType.softRevoked, state.getType()); + assertTrue(state.isSoftRevocation()); + assertFalse(state.isHardRevocation()); + assertFalse(state.isNotRevoked()); + assertEquals(date, state.getDate()); + + assertEquals("softRevoked (2022-08-03 18:26:35 UTC)", state.toString()); + } + + @Test + public void orderTest() { + assertEquals(RevocationState.notRevoked(), RevocationState.notRevoked()); + assertEquals(RevocationState.hardRevoked(), RevocationState.hardRevoked()); + Date now = new Date(); + assertEquals(RevocationState.softRevoked(now), RevocationState.softRevoked(now)); + + assertEquals(1, RevocationState.softRevoked(now).compareTo(RevocationState.notRevoked())); + + assertEquals(0, RevocationState.notRevoked().compareTo(RevocationState.notRevoked())); + assertEquals(0, RevocationState.hardRevoked().compareTo(RevocationState.hardRevoked())); + assertTrue(RevocationState.hardRevoked().compareTo(RevocationState.notRevoked()) > 0); + + List states = new ArrayList<>(); + RevocationState earlySoft = RevocationState.softRevoked(DateUtil.parseUTCDate("2000-05-12 10:44:01 UTC")); + RevocationState laterSoft = RevocationState.softRevoked(DateUtil.parseUTCDate("2022-08-03 18:26:35 UTC")); + RevocationState hard = RevocationState.hardRevoked(); + RevocationState not = RevocationState.notRevoked(); + RevocationState not2 = RevocationState.notRevoked(); + states.add(laterSoft); + states.add(not); + states.add(not2); + states.add(hard); + states.add(earlySoft); + + Collections.shuffle(states); + Collections.sort(states); + + assertEquals(states, Arrays.asList(not, not2, laterSoft, earlySoft, hard)); + } + + @SuppressWarnings({"SimplifiableAssertion", "ConstantConditions", "EqualsWithItself", "EqualsBetweenInconvertibleTypes"}) + @Test + public void equalsTest() { + RevocationState rev = RevocationState.hardRevoked(); + assertFalse(rev.equals(null)); + assertTrue(rev.equals(rev)); + assertFalse(rev.equals("not a revocation")); + RevocationState other = RevocationState.notRevoked(); + assertFalse(rev.equals(other)); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/algorithm/SignatureTypeTest.java b/pgpainless-core/src/test/java/org/pgpainless/algorithm/SignatureTypeTest.java index 1bf78776..7f2bc2a6 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/algorithm/SignatureTypeTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/algorithm/SignatureTypeTest.java @@ -5,7 +5,6 @@ package org.pgpainless.algorithm; import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.Test; @@ -31,6 +30,6 @@ public class SignatureTypeTest { assertFalse(SignatureType.isRevocationSignature(SignatureType.STANDALONE.getCode())); assertFalse(SignatureType.isRevocationSignature(SignatureType.TIMESTAMP.getCode())); - assertThrows(IllegalArgumentException.class, () -> SignatureType.isRevocationSignature(-3)); + assertFalse(SignatureType.isRevocationSignature(-3)); } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/algorithm/TrustworthinessTest.java b/pgpainless-core/src/test/java/org/pgpainless/algorithm/TrustworthinessTest.java new file mode 100644 index 00000000..bf87ed65 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/algorithm/TrustworthinessTest.java @@ -0,0 +1,87 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.algorithm; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +public class TrustworthinessTest { + + @Test + public void fullyTrustedIntroducer() { + Trustworthiness it = Trustworthiness.fullyTrusted().introducer(); + assertTrue(it.isFullyTrusted()); + assertFalse(it.isNotTrusted()); + + assertTrue(it.isIntroducer()); + assertFalse(it.canIntroduce(it)); + } + + @Test + public void marginallyTrustedIntroducer() { + Trustworthiness it = Trustworthiness.marginallyTrusted().introducer(); + assertFalse(it.isFullyTrusted()); + assertTrue(it.isMarginallyTrusted()); + assertFalse(it.isNotTrusted()); + + assertTrue(it.isIntroducer()); + assertFalse(it.canIntroduce(2)); + } + + @Test + public void nonTrustedIntroducer() { + Trustworthiness it = Trustworthiness.untrusted().introducer(); + assertTrue(it.isNotTrusted()); + assertFalse(it.isMarginallyTrusted()); + assertFalse(it.isFullyTrusted()); + + assertTrue(it.isIntroducer()); + } + + @Test + public void trustedMetaIntroducer() { + Trustworthiness it = Trustworthiness.fullyTrusted().metaIntroducer(); + assertTrue(it.isFullyTrusted()); + assertTrue(it.isIntroducer()); + + Trustworthiness that = Trustworthiness.fullyTrusted().introducer(); + assertTrue(it.canIntroduce(that)); + assertFalse(that.canIntroduce(it)); + } + + @Test + public void invalidArguments() { + assertThrows(IllegalArgumentException.class, () -> new Trustworthiness(300, 1)); + assertThrows(IllegalArgumentException.class, () -> new Trustworthiness(60, 300)); + assertThrows(IllegalArgumentException.class, () -> new Trustworthiness(-4, 1)); + assertThrows(IllegalArgumentException.class, () -> new Trustworthiness(120, -1)); + } + + @Test + public void inBetweenValues() { + Trustworthiness it = new Trustworthiness(30, 1); + assertTrue(it.isMarginallyTrusted()); + assertFalse(it.isFullyTrusted()); + + it = new Trustworthiness(140, 1); + assertTrue(it.isFullyTrusted()); + } + + @Test + public void depthHierarchyTest() { + Trustworthiness l1 = Trustworthiness.fullyTrusted().metaIntroducerOfDepth(1); + Trustworthiness l2 = Trustworthiness.fullyTrusted().metaIntroducerOfDepth(2); + Trustworthiness l3 = Trustworthiness.fullyTrusted().metaIntroducerOfDepth(3); + + assertTrue(l3.canIntroduce(l2)); + assertTrue(l3.canIntroduce(l1)); + assertTrue(l2.canIntroduce(l1)); + assertFalse(l1.canIntroduce(l2)); + assertFalse(l1.canIntroduce(l3)); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CanonicalizedDataEncryptionTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CanonicalizedDataEncryptionTest.java new file mode 100644 index 00000000..f9936db0 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CanonicalizedDataEncryptionTest.java @@ -0,0 +1,459 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.stream.Stream; + +import org.bouncycastle.bcpg.ArmoredOutputStream; +import org.bouncycastle.bcpg.CompressionAlgorithmTags; +import org.bouncycastle.openpgp.PGPCompressedDataGenerator; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPLiteralData; +import org.bouncycastle.openpgp.PGPLiteralDataGenerator; +import org.bouncycastle.openpgp.PGPOnePassSignature; +import org.bouncycastle.openpgp.PGPPrivateKey; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.PGPSignatureGenerator; +import org.bouncycastle.openpgp.operator.bc.BcPGPContentSignerBuilder; +import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.DocumentSignatureType; +import org.pgpainless.algorithm.HashAlgorithm; +import org.pgpainless.algorithm.StreamEncoding; +import org.pgpainless.encryption_signing.CRLFGeneratorStream; +import org.pgpainless.encryption_signing.EncryptionOptions; +import org.pgpainless.encryption_signing.EncryptionStream; +import org.pgpainless.encryption_signing.ProducerOptions; +import org.pgpainless.encryption_signing.SigningOptions; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.key.protection.UnlockSecretKey; + +public class CanonicalizedDataEncryptionTest { + + private static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 9AF4 29C4 C389 CC11 1739 98E9 9F8E E9C5 3AE5 C1A4\n" + + "Comment: Test \n" + + "\n" + + "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\n" + + "=EyDf\n" + + "-----END PGP PRIVATE KEY BLOCK-----\n"; + + String message = "Hello, World!\n"; + + private static PGPSecretKeyRing secretKeys; + private static PGPPublicKeyRing publicKeys; + + @BeforeAll + public static void readKeys() throws IOException { + secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); + publicKeys = PGPainless.extractCertificate(secretKeys); + // CHECKSTYLE:OFF + System.out.println(PGPainless.asciiArmor(secretKeys)); + // CHECKSTYLE:ON + } + + // NO CR/LF ENCODING PRIOR TO PROCESSING + + @Test + public void noInputEncodingBinaryDataBinarySig() throws PGPException, IOException { + String msg = encryptAndSign(message, DocumentSignatureType.BINARY_DOCUMENT, StreamEncoding.BINARY, false); + MessageMetadata metadata = decryptAndVerify(msg); + + if (!metadata.isVerifiedSigned()) { + // CHECKSTYLE:OFF + System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); + System.out.println(msg); + // CHECKSTYLE:ON + fail(); + } + } + + @Test + public void noInputEncodingBinaryDataTextSig() throws PGPException, IOException { + String msg = encryptAndSign(message, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT, StreamEncoding.BINARY, false); + MessageMetadata metadata = decryptAndVerify(msg); + + if (!metadata.isVerifiedSigned()) { + // CHECKSTYLE:OFF + System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); + System.out.println(msg); + // CHECKSTYLE:ON + fail(); + } + } + + @Test + public void noInputEncodingTextDataBinarySig() throws PGPException, IOException { + String msg = encryptAndSign(message, DocumentSignatureType.BINARY_DOCUMENT, StreamEncoding.TEXT, false); + MessageMetadata metadata = decryptAndVerify(msg); + + if (!metadata.isVerifiedSigned()) { + // CHECKSTYLE:OFF + System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); + System.out.println(msg); + // CHECKSTYLE:ON + fail(); + } + } + + @Test + public void noInputEncodingTextDataTextSig() throws PGPException, IOException { + String msg = encryptAndSign(message, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT, StreamEncoding.TEXT, false); + MessageMetadata metadata = decryptAndVerify(msg); + + if (!metadata.isVerifiedSigned()) { + // CHECKSTYLE:OFF + System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); + System.out.println(msg); + // CHECKSTYLE:ON + fail(); + } + } + + @Test + public void noInputEncodingUtf8DataBinarySig() throws PGPException, IOException { + String msg = encryptAndSign(message, DocumentSignatureType.BINARY_DOCUMENT, StreamEncoding.UTF8, false); + MessageMetadata metadata = decryptAndVerify(msg); + + if (!metadata.isVerifiedSigned()) { + // CHECKSTYLE:OFF + System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); + System.out.println(msg); + // CHECKSTYLE:ON + fail(); + } + } + + @Test + public void noInputEncodingUtf8DataTextSig() throws PGPException, IOException { + String msg = encryptAndSign(message, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT, StreamEncoding.UTF8, false); + MessageMetadata metadata = decryptAndVerify(msg); + + if (!metadata.isVerifiedSigned()) { + // CHECKSTYLE:OFF + System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); + System.out.println(msg); + // CHECKSTYLE:ON + fail(); + } + } + // APPLY CR/LF ENCODING PRIOR TO PROCESSING + + @Test + public void inputEncodingBinaryDataBinarySig() throws PGPException, IOException { + String msg = encryptAndSign(message, DocumentSignatureType.BINARY_DOCUMENT, StreamEncoding.BINARY, true); + MessageMetadata metadata = decryptAndVerify(msg); + + if (!metadata.isVerifiedSigned()) { + // CHECKSTYLE:OFF + System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); + System.out.println(msg); + // CHECKSTYLE:ON + fail(); + } + } + + @Test + public void inputEncodingBinaryDataTextSig() throws PGPException, IOException { + String msg = encryptAndSign(message, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT, StreamEncoding.BINARY, true); + MessageMetadata metadata = decryptAndVerify(msg); + + if (!metadata.isVerifiedSigned()) { + // CHECKSTYLE:OFF + System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); + System.out.println(msg); + // CHECKSTYLE:ON + fail(); + } + } + + @Test + public void inputEncodingTextDataBinarySig() throws PGPException, IOException { + String msg = encryptAndSign(message, DocumentSignatureType.BINARY_DOCUMENT, StreamEncoding.TEXT, true); + MessageMetadata metadata = decryptAndVerify(msg); + + if (!metadata.isVerifiedSigned()) { + // CHECKSTYLE:OFF + System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); + System.out.println(msg); + // CHECKSTYLE:ON + fail(); + } + } + + @Test + public void inputEncodingTextDataTextSig() throws PGPException, IOException { + String msg = encryptAndSign(message, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT, StreamEncoding.TEXT, true); + MessageMetadata metadata = decryptAndVerify(msg); + + if (!metadata.isVerifiedSigned()) { + // CHECKSTYLE:OFF + System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); + System.out.println(msg); + // CHECKSTYLE:ON + fail(); + } + } + + @Test + public void inputEncodingUtf8DataBinarySig() throws PGPException, IOException { + String msg = encryptAndSign(message, DocumentSignatureType.BINARY_DOCUMENT, StreamEncoding.UTF8, true); + MessageMetadata metadata = decryptAndVerify(msg); + + if (!metadata.isVerifiedSigned()) { + // CHECKSTYLE:OFF + System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); + System.out.println(msg); + // CHECKSTYLE:ON + fail(); + } + } + + @Test + public void inputEncodingUtf8DataTextSig() throws PGPException, IOException { + String msg = encryptAndSign(message, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT, StreamEncoding.UTF8, true); + MessageMetadata metadata = decryptAndVerify(msg); + + if (!metadata.isVerifiedSigned()) { + // CHECKSTYLE:OFF + System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); + System.out.println(msg); + // CHECKSTYLE:ON + fail(); + } + } + + @ParameterizedTest + @MethodSource("resultOfDecryptionIsCRLFEncodedArguments") + public void resultOfDecryptionIsCRLFEncoded(String before, String after) throws PGPException, IOException { + String encrypted = encryptAndSign(before, DocumentSignatureType.BINARY_DOCUMENT, StreamEncoding.TEXT, true); + + ByteArrayInputStream in = new ByteArrayInputStream(encrypted.getBytes(StandardCharsets.UTF_8)); + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(in) + .withOptions(new ConsumerOptions() + .addDecryptionKey(secretKeys, SecretKeyRingProtector.unprotectedKeys()) + .addVerificationCert(publicKeys)); + + ByteArrayOutputStream decrypted = new ByteArrayOutputStream(); + Streams.pipeAll(decryptionStream, decrypted); + decryptionStream.close(); + + assertArrayEquals(after.getBytes(StandardCharsets.UTF_8), decrypted.toByteArray()); + } + + private static Stream resultOfDecryptionIsCRLFEncodedArguments() { + return Stream.of( + Arguments.of("foo", "foo"), + Arguments.of("rrr", "rrr"), + Arguments.of("Foo\nBar!\n", "Foo\r\nBar!\r\n"), + Arguments.of("Foo\rBar!\r", "Foo\r\nBar!\r\n"), + Arguments.of("Foo\r\nBar!\r\n", "Foo\r\nBar!\r\n") + ); + } + + @Test + public void resultOfDecryptionIsNotCRLFEncoded() throws PGPException, IOException { + String beforeAndAfter = "Foo\nBar!\n"; + + String encrypted = encryptAndSign(beforeAndAfter, DocumentSignatureType.BINARY_DOCUMENT, StreamEncoding.TEXT, false); + + ByteArrayInputStream in = new ByteArrayInputStream(encrypted.getBytes(StandardCharsets.UTF_8)); + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(in) + .withOptions(new ConsumerOptions() + .addDecryptionKey(secretKeys, SecretKeyRingProtector.unprotectedKeys()) + .addVerificationCert(publicKeys)); + + ByteArrayOutputStream decrypted = new ByteArrayOutputStream(); + Streams.pipeAll(decryptionStream, decrypted); + decryptionStream.close(); + + assertArrayEquals(beforeAndAfter.getBytes(StandardCharsets.UTF_8), decrypted.toByteArray()); + } + + private String encryptAndSign(String message, + DocumentSignatureType sigType, + StreamEncoding dataFormat, + boolean applyCRLFEncoding) + throws PGPException, IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + ProducerOptions options = ProducerOptions + .signAndEncrypt( + EncryptionOptions.encryptCommunications() + .addRecipient(publicKeys), + SigningOptions.get() + .addInlineSignature(SecretKeyRingProtector.unprotectedKeys(), secretKeys, sigType) + ) + .setEncoding(dataFormat); + if (applyCRLFEncoding) { + options.applyCRLFEncoding(); + } + + EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() + .onOutputStream(out) + .withOptions(options); + + ByteArrayInputStream inputStream = new ByteArrayInputStream(message.getBytes(StandardCharsets.UTF_8)); + Streams.pipeAll(inputStream, encryptionStream); + encryptionStream.close(); + + String msg = out.toString(); + return msg; + } + + private MessageMetadata decryptAndVerify(String msg) throws PGPException, IOException { + ByteArrayInputStream in = new ByteArrayInputStream(msg.getBytes(StandardCharsets.UTF_8)); + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(in) + .withOptions(new ConsumerOptions() + .addDecryptionKey(secretKeys, SecretKeyRingProtector.unprotectedKeys()) + .addVerificationCert(publicKeys)); + + Streams.drain(decryptionStream); + decryptionStream.close(); + + return decryptionStream.getMetadata(); + } + + @Test + public void testManualSignWithAllCombinations() throws PGPException, IOException { + for (StreamEncoding streamEncoding : StreamEncoding.values()) { + for (DocumentSignatureType sigType : DocumentSignatureType.values()) { + manualSignAndVerify(sigType, streamEncoding); + } + } + } + + public void manualSignAndVerify(DocumentSignatureType sigType, StreamEncoding streamEncoding) + throws IOException, PGPException { + PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKeys.getSecretKey(), SecretKeyRingProtector.unprotectedKeys()); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ArmoredOutputStream armorOut = new ArmoredOutputStream(out); + + PGPCompressedDataGenerator compressor = new PGPCompressedDataGenerator(CompressionAlgorithmTags.ZLIB); + OutputStream compressedOut = compressor.open(armorOut); + + PGPSignatureGenerator signer = new PGPSignatureGenerator( + new BcPGPContentSignerBuilder( + secretKeys.getPublicKey().getAlgorithm(), + HashAlgorithm.SHA256.getAlgorithmId())); + signer.init(sigType.getSignatureType().getCode(), privateKey); + + PGPOnePassSignature ops = signer.generateOnePassVersion(false); + ops.encode(compressedOut); + + PGPLiteralDataGenerator author = new PGPLiteralDataGenerator(); + OutputStream literalOut = author.open(compressedOut, streamEncoding.getCode(), "", PGPLiteralData.NOW, new byte[4096]); + + byte[] msg = message.getBytes(StandardCharsets.UTF_8); + + ByteArrayOutputStream crlfed = new ByteArrayOutputStream(); + CRLFGeneratorStream crlfOut = new CRLFGeneratorStream(crlfed, streamEncoding); + crlfOut.write(msg); + msg = crlfed.toByteArray(); + + for (byte b : msg) { + literalOut.write(b); + signer.update(b); + } + + literalOut.close(); + PGPSignature signature = signer.generate(); + + signature.encode(compressedOut); + compressor.close(); + + armorOut.close(); + + String ciphertext = out.toString(); + // CHECKSTYLE:OFF + System.out.println(sigType + " " + streamEncoding); + System.out.println(ciphertext); + // CHECKSTYLE:ON + + ByteArrayInputStream cipherIn = new ByteArrayInputStream(ciphertext.getBytes(StandardCharsets.UTF_8)); + ByteArrayOutputStream decrypted = new ByteArrayOutputStream(); + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(cipherIn) + .withOptions(new ConsumerOptions() + .addVerificationCert(publicKeys)); + + Streams.pipeAll(decryptionStream, decrypted); + decryptionStream.close(); + MessageMetadata metadata = decryptionStream.getMetadata(); + assertTrue(metadata.isVerifiedSigned(), "Not verified! Sig Type: " + sigType + " StreamEncoding: " + streamEncoding); + + assertArrayEquals(msg, decrypted.toByteArray()); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CertificateWithMissingSecretKeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CertificateWithMissingSecretKeyTest.java new file mode 100644 index 00000000..e5f9e370 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CertificateWithMissingSecretKeyTest.java @@ -0,0 +1,147 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.DocumentSignatureType; +import org.pgpainless.algorithm.EncryptionPurpose; +import org.pgpainless.encryption_signing.EncryptionOptions; +import org.pgpainless.encryption_signing.EncryptionStream; +import org.pgpainless.encryption_signing.ProducerOptions; +import org.pgpainless.encryption_signing.SigningOptions; +import org.pgpainless.exception.KeyException; +import org.pgpainless.exception.MissingDecryptionMethodException; +import org.pgpainless.key.info.KeyRingInfo; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.key.util.KeyRingUtils; + +public class CertificateWithMissingSecretKeyTest { + + private static final String MISSING_SIGNING_SECKEY = "" + + "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: E97B 15E6 52FA 8BAE 2311 DDCB A5BD 9DC4 4415 C987\n" + + "Comment: Missing Signing Subkey \n" + + "\n" + + "lFgEYjCuERYJKwYBBAHaRw8BAQdAaqeTdbyb/D+UXd2aXsP58+k+tvt22DnL6bC0\n" + + "7p2tJacAAP0fEmwUY7rSPugQakzsA8nV4Nv3PYlKa6meqEePT+8s8BFitC9NaXNz\n" + + "aW5nIFNpZ25pbmcgU3Via2V5IDxtaXNzaW5nQHNpZ25pbmcuc3Via2V5PoiPBBMW\n" + + "CgBBBQJiMK4RCRClvZ3ERBXJhxYhBOl7FeZS+ouuIxHdy6W9ncREFcmHAp4BApsB\n" + + "BRYCAwEABAsJCAcFFQoJCAsCmQEAAN0HAPkB7IphgTM94s/VpyV5+hvYbxesnji5\n" + + "RNzqs3nRhS8DBgEA/+gCpAkgznB3T/uNtWIoTf7Kuib5mIJ+SW0l+htuEgacXQRi\n" + + "MK4REgorBgEEAZdVAQUBAQdAlaQH44c7PdKkjaVVXvg86i+thKV121C/nH75Krhh\n" + + "QxYDAQgHAAD/aWJt9M85Al+57lPqS5ppzrIoCoTZ6JCwuJUSNEAg4BgQ6Ih1BBgW\n" + + "CgAdBQJiMK4RAp4BApsMBRYCAwEABAsJCAcFFQoJCAsACgkQpb2dxEQVyYdzuAD9\n" + + "GEkU7NfugHw8alQT7IJbUobVyZzeXQyzPqSKUw/Vp54BAJXZj8NzQrrM4Q5C3+Mf\n" + + "uznN+ryRovDXhf8T5PUXHloDuDMEYjCuERYJKwYBBAHaRw8BAQdAVeBpPurrwAU3\n" + + "ns+1C2c6wJ8iTZ1eWEP2qgBAlokx5N+I1QQYFgoAfQUCYjCuEQKeAQKbAgUWAgMB\n" + + "AAQLCQgHBRUKCQgLXyAEGRYKAAYFAmIwrhEACgkQld4KwYO6xR4YEwEA942iduoW\n" + + "1ANEmwCwnYwMAa3HlXsMs5bdIUGnxuo7MBEA/1YYeAu45O2Z8kTdrDZM/1emoxt1\n" + + "j6EzybnaJ+2XGX4AAAoJEKW9ncREFcmHLXsBAITCIwGtaCvZdWCQlJeYak1NTuBp\n" + + "bmOEFga0sLmRI/zYAP97U2oc8dqV55S1b4yNkfENK2MD6Ow0nv8CL6+S0UaCBw==\n" + + "=eTh7\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + + private static final long signingSubkeyId = -7647663290973502178L; + private static PGPSecretKeyRing missingSigningSecKey; + + private static long encryptionSubkeyId; + private static PGPSecretKeyRing missingDecryptionSecKey; + + private static final SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); + + + @BeforeAll + public static void prepare() throws IOException, PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + // missing signing sec key we read from bytes + missingSigningSecKey = PGPainless.readKeyRing().secretKeyRing(MISSING_SIGNING_SECKEY); + + // missing encryption sec key we generate on the fly + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() + .modernKeyRing("Missing Decryption Key "); + encryptionSubkeyId = PGPainless.inspectKeyRing(secretKeys) + .getEncryptionSubkeys(EncryptionPurpose.ANY).get(0).getKeyID(); + // remove the encryption/decryption secret key + missingDecryptionSecKey = KeyRingUtils.stripSecretKey(secretKeys, encryptionSubkeyId); + } + + @Test + public void assureMissingSigningSecKeyOnlyContainSigningPubKey() { + assertNotNull(missingSigningSecKey.getPublicKey(signingSubkeyId)); + assertNull(missingSigningSecKey.getSecretKey(signingSubkeyId)); + + KeyRingInfo info = PGPainless.inspectKeyRing(missingSigningSecKey); + assertFalse(info.getSigningSubkeys().isEmpty()); // This method only tests for pub keys. + } + + @Test + public void assureMissingDecryptionSecKeyOnlyContainsEncryptionPubKey() { + assertNotNull(missingDecryptionSecKey.getPublicKey(encryptionSubkeyId)); + assertNull(missingDecryptionSecKey.getSecretKey(encryptionSubkeyId)); + + KeyRingInfo info = PGPainless.inspectKeyRing(missingDecryptionSecKey); + assertFalse(info.getEncryptionSubkeys(EncryptionPurpose.ANY).isEmpty()); // pub key is still there + } + + @Test + public void testSignWithMissingSigningSecKey() { + SigningOptions signingOptions = SigningOptions.get(); + + assertThrows(KeyException.MissingSecretKeyException.class, () -> + signingOptions.addInlineSignature(protector, missingSigningSecKey, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT)); + assertThrows(KeyException.MissingSecretKeyException.class, () -> + signingOptions.addDetachedSignature(protector, missingSigningSecKey, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT)); + } + + @Test + public void testEncryptDecryptWithMissingDecryptionKey() throws PGPException, IOException { + ByteArrayInputStream in = new ByteArrayInputStream("Hello, World!\n".getBytes(StandardCharsets.UTF_8)); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + PGPPublicKeyRing certificate = PGPainless.extractCertificate(missingDecryptionSecKey); + ProducerOptions producerOptions = ProducerOptions.encrypt( + EncryptionOptions.encryptCommunications() + .addRecipient(certificate)); // we can still encrypt, since the pub key is still there + + EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() + .onOutputStream(out) + .withOptions(producerOptions); + + Streams.pipeAll(in, encryptionStream); + encryptionStream.close(); + + assertTrue(encryptionStream.getResult().isEncryptedFor(certificate)); + + // Test decryption + ByteArrayInputStream cipherIn = new ByteArrayInputStream(out.toByteArray()); + + ConsumerOptions consumerOptions = new ConsumerOptions() + .addDecryptionKey(missingDecryptionSecKey); + + assertThrows(MissingDecryptionMethodException.class, () -> + PGPainless.decryptAndOrVerify() + .onInputStream(cipherIn) + .withOptions(consumerOptions)); // <- cannot find decryption key + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CleartextSignatureVerificationTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CleartextSignatureVerificationTest.java index c355438d..720a0d53 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CleartextSignatureVerificationTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CleartextSignatureVerificationTest.java @@ -14,7 +14,11 @@ import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; +import java.io.InputStream; import java.nio.charset.StandardCharsets; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.util.Random; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKey; @@ -25,7 +29,7 @@ import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.DocumentSignatureType; -import org.pgpainless.decryption_verification.cleartext_signatures.CleartextSignatureProcessor; +import org.pgpainless.decryption_verification.cleartext_signatures.ClearsignedMessageUtil; import org.pgpainless.decryption_verification.cleartext_signatures.InMemoryMultiPassStrategy; import org.pgpainless.decryption_verification.cleartext_signatures.MultiPassStrategy; import org.pgpainless.encryption_signing.EncryptionStream; @@ -34,9 +38,9 @@ import org.pgpainless.encryption_signing.SigningOptions; import org.pgpainless.exception.WrongConsumingMethodException; import org.pgpainless.key.TestKeys; import org.pgpainless.key.protection.SecretKeyRingProtector; -import org.pgpainless.signature.CertificateValidator; +import org.pgpainless.signature.consumer.CertificateValidator; import org.pgpainless.signature.SignatureUtils; -import org.pgpainless.signature.SignatureVerifier; +import org.pgpainless.signature.consumer.SignatureVerifier; import org.pgpainless.util.ArmorUtils; import org.pgpainless.util.TestUtils; @@ -72,34 +76,39 @@ public class CleartextSignatureVerificationTest { "=Z2SO\n" + "-----END PGP SIGNATURE-----").getBytes(StandardCharsets.UTF_8); + public static final String alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + public static final Random random = new Random(); + @Test - public void cleartextSignVerification_InMemoryMultiPassStrategy() throws IOException, PGPException { + public void cleartextSignVerification_InMemoryMultiPassStrategy() + throws IOException, PGPException { PGPPublicKeyRing signingKeys = TestKeys.getEmilPublicKeyRing(); ConsumerOptions options = new ConsumerOptions() .addVerificationCert(signingKeys); InMemoryMultiPassStrategy multiPassStrategy = MultiPassStrategy.keepMessageInMemory(); - CleartextSignatureProcessor processor = PGPainless.verifyCleartextSignedMessage() + options.setMultiPassStrategy(multiPassStrategy); + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() .onInputStream(new ByteArrayInputStream(MESSAGE_SIGNED)) - .withStrategy(multiPassStrategy) .withOptions(options); - DecryptionStream decryptionStream = processor.getVerificationStream(); ByteArrayOutputStream out = new ByteArrayOutputStream(); Streams.pipeAll(decryptionStream, out); decryptionStream.close(); - OpenPgpMetadata result = decryptionStream.getResult(); - assertTrue(result.isVerified()); + MessageMetadata result = decryptionStream.getMetadata(); + assertTrue(result.isVerifiedSigned()); + assertTrue(result.isUsingCleartextSignatureFramework()); - PGPSignature signature = result.getVerifiedSignatures().values().iterator().next(); + PGPSignature signature = result.getVerifiedSignatures().iterator().next().getSignature(); assertEquals(signature.getKeyID(), signingKeys.getPublicKey().getKeyID()); assertArrayEquals(MESSAGE_BODY, out.toByteArray()); } @Test - public void cleartextSignVerification_FileBasedMultiPassStrategy() throws IOException, PGPException { + public void cleartextSignVerification_FileBasedMultiPassStrategy() + throws IOException, PGPException { PGPPublicKeyRing signingKeys = TestKeys.getEmilPublicKeyRing(); ConsumerOptions options = new ConsumerOptions() .addVerificationCert(signingKeys); @@ -107,21 +116,19 @@ public class CleartextSignatureVerificationTest { File tempDir = TestUtils.createTempDirectory(); File file = new File(tempDir, "file"); MultiPassStrategy multiPassStrategy = MultiPassStrategy.writeMessageToFile(file); - CleartextSignatureProcessor processor = PGPainless.verifyCleartextSignedMessage() + options.setMultiPassStrategy(multiPassStrategy); + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() .onInputStream(new ByteArrayInputStream(MESSAGE_SIGNED)) - .withStrategy(multiPassStrategy) .withOptions(options); - DecryptionStream decryptionStream = processor.getVerificationStream(); - ByteArrayOutputStream out = new ByteArrayOutputStream(); Streams.pipeAll(decryptionStream, out); decryptionStream.close(); - OpenPgpMetadata result = decryptionStream.getResult(); - assertTrue(result.isVerified()); + MessageMetadata result = decryptionStream.getMetadata(); + assertTrue(result.isVerifiedSigned()); - PGPSignature signature = result.getVerifiedSignatures().values().iterator().next(); + PGPSignature signature = result.getVerifiedSignatures().iterator().next().getSignature(); assertEquals(signature.getKeyID(), signingKeys.getPublicKey().getKeyID()); FileInputStream fileIn = new FileInputStream(file); @@ -132,7 +139,8 @@ public class CleartextSignatureVerificationTest { } @Test - public void verifySignatureDetached() throws IOException, PGPException { + public void verifySignatureDetached() + throws IOException, PGPException { PGPPublicKeyRing signingKeys = TestKeys.getEmilPublicKeyRing(); PGPSignature signature = SignatureUtils.readSignatures(SIGNATURE).get(0); @@ -154,7 +162,8 @@ public class CleartextSignatureVerificationTest { } @Test - public void testOutputOfSigVerification() throws IOException, PGPException { + public void testOutputOfSigVerification() + throws IOException, PGPException { PGPSignature signature = SignatureUtils.readSignatures(SIGNATURE).get(0); ConsumerOptions options = new ConsumerOptions() @@ -169,61 +178,21 @@ public class CleartextSignatureVerificationTest { Streams.pipeAll(decryptionStream, out); decryptionStream.close(); - OpenPgpMetadata metadata = decryptionStream.getResult(); + MessageMetadata metadata = decryptionStream.getMetadata(); assertEquals(1, metadata.getVerifiedSignatures().size()); } @Test - public void consumingCleartextSignedMessageWithNormalAPIThrowsWrongConsumingMethodException() throws IOException, PGPException { - PGPPublicKeyRing certificate = TestKeys.getEmilPublicKeyRing(); - ConsumerOptions options = new ConsumerOptions() - .addVerificationCert(certificate); - - assertThrows(WrongConsumingMethodException.class, () -> - PGPainless.decryptAndOrVerify() - .onInputStream(new ByteArrayInputStream(MESSAGE_SIGNED)) - .withOptions(options)); - } - - @Test - public void consumingInlineSignedMessageWithCleartextSignedVerificationApiThrowsWrongConsumingMethodException() throws PGPException, IOException { - String inlineSignedMessage = "-----BEGIN PGP MESSAGE-----\n" + - "Version: PGPainless\n" + - "\n" + - "kA0DAQoTVzbmkxrPNwwBy8BJYgAAAAAAQWgsIEp1bGlldCwgaWYgdGhlIG1lYXN1\n" + - "cmUgb2YgdGh5IGpveQpCZSBoZWFwZWQgbGlrZSBtaW5lLCBhbmQgdGhhdCB0aHkg\n" + - "c2tpbGwgYmUgbW9yZQpUbyBibGF6b24gaXQsIHRoZW4gc3dlZXRlbiB3aXRoIHRo\n" + - "eSBicmVhdGgKVGhpcyBuZWlnaGJvciBhaXIsIGFuZCBsZXQgcmljaCBtdXNpY+KA\n" + - "mXMgdG9uZ3VlClVuZm9sZCB0aGUgaW1hZ2luZWQgaGFwcGluZXNzIHRoYXQgYm90\n" + - "aApSZWNlaXZlIGluIGVpdGhlciBieSB0aGlzIGRlYXIgZW5jb3VudGVyLoh1BAET\n" + - "CgAGBQJhK2q9ACEJEFc25pMazzcMFiEET2ZcTcLEZgvGQl5BVzbmkxrPNwxr8gD+\n" + - "MDfg+qccpsoJVgHIW8mRPBQowXDyw+oNHsf28ii+/pEBAO/RXhFkZBPzlfDJMJVT\n" + - "UwJJeuna1R4yOoWjq0zqRvrg\n" + - "=dBiV\n" + - "-----END PGP MESSAGE-----\n"; - - PGPPublicKeyRing certificate = TestKeys.getEmilPublicKeyRing(); - ConsumerOptions options = new ConsumerOptions() - .addVerificationCert(certificate); - - assertThrows(WrongConsumingMethodException.class, () -> - PGPainless.verifyCleartextSignedMessage() - .onInputStream(new ByteArrayInputStream(inlineSignedMessage.getBytes(StandardCharsets.UTF_8))) - .withStrategy(new InMemoryMultiPassStrategy()) - .withOptions(options) - .getVerificationStream()); - } - - @Test - public void getDecoderStreamMistakensPlaintextForBase64RegressionTest() throws PGPException, IOException { - String message = "Foo\nBar"; // PGPUtil.getDecoderStream() would mistaken this for base64 data + public void getDecoderStreamMistakensPlaintextForBase64RegressionTest() + throws PGPException, IOException { + String message = "Foo\nBar"; // PGPUtil.getDecoderStream() would have mistaken this for base64 data ByteArrayInputStream msgIn = new ByteArrayInputStream(message.getBytes(StandardCharsets.UTF_8)); PGPSecretKeyRing secretKey = TestKeys.getEmilSecretKeyRing(); ByteArrayOutputStream signedOut = new ByteArrayOutputStream(); EncryptionStream signingStream = PGPainless.encryptAndOrSign().onOutputStream(signedOut) .withOptions(ProducerOptions.sign(SigningOptions.get() - .addDetachedSignature(SecretKeyRingProtector.unprotectedKeys(), secretKey, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT)) + .addDetachedSignature(SecretKeyRingProtector.unprotectedKeys(), secretKey, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT)) .setCleartextSigned()); Streams.pipeAll(msgIn, signingStream); @@ -232,18 +201,91 @@ public class CleartextSignatureVerificationTest { String signed = signedOut.toString(); ByteArrayInputStream signedIn = new ByteArrayInputStream(signed.getBytes(StandardCharsets.UTF_8)); - DecryptionStream verificationStream = PGPainless.verifyCleartextSignedMessage() + DecryptionStream verificationStream = PGPainless.decryptAndOrVerify() .onInputStream(signedIn) - .withStrategy(new InMemoryMultiPassStrategy()) .withOptions(new ConsumerOptions() - .addVerificationCert(TestKeys.getEmilPublicKeyRing())) - .getVerificationStream(); + .addVerificationCert(TestKeys.getEmilPublicKeyRing())); ByteArrayOutputStream msgOut = new ByteArrayOutputStream(); Streams.pipeAll(verificationStream, msgOut); verificationStream.close(); - OpenPgpMetadata metadata = verificationStream.getResult(); - assertTrue(metadata.isVerified()); + MessageMetadata metadata = verificationStream.getMetadata(); + assertTrue(metadata.isVerifiedSigned()); + } + + @Test + public void testDecryptionOfVeryLongClearsignedMessage() + throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + String message = randomString(28, 4000); + + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().modernKeyRing("Alice"); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() + .onOutputStream(out) + .withOptions(ProducerOptions.sign( + SigningOptions.get() + .addDetachedSignature(SecretKeyRingProtector.unprotectedKeys(), + secretKeys, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT) + ).setCleartextSigned()); + + Streams.pipeAll(new ByteArrayInputStream(message.getBytes(StandardCharsets.UTF_8)), encryptionStream); + encryptionStream.close(); + + String cleartextSigned = out.toString(); + + ByteArrayInputStream in = new ByteArrayInputStream(cleartextSigned.getBytes(StandardCharsets.UTF_8)); + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(in) + .withOptions(new ConsumerOptions() + .addVerificationCert(PGPainless.extractCertificate(secretKeys))); + + out = new ByteArrayOutputStream(); + Streams.pipeAll(decryptionStream, out); + decryptionStream.close(); + } + + @Test + public void clearsignedMessageUtil_detachSignaturesFromInbandNonClearsignedMessageThrows() { + // Message is inband signed, but does not use cleartext signature framework + String message = "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "owGbwMvMyCX29UzQdZ1/lUqMpw8YJDGAgJGjd3JgcqJTVUpylpOCmUK+l39asYGl\n" + + "k1NkcYSxgkuaR26EQplppGVuREGqn3NBRJRXoVm4T1BuhoJjcllOYV5xhmVKloVz\n" + + "UJaZQmhBSbqCr6uhQlVIkL9rqUJgaaWjpalCuVdiXkVhiFNuQHpmeLpChGNqVkG5\n" + + "U1iBgqmvo79LXlFVWK5rpEGkh0dBfrB/ngKXj5FhVlZuUpllTk6xb3m5QlWUT3Gh\n" + + "o7dCQXGIgnlwZkBYlI9FhEFAprdnkLGFe6KjZ2meQblCXkiWaWhUknl5YmmYb7JC\n" + + "noJJeWZYXmJarpFvXkpKpbGXQkcpC6MYF4M6K1PShlmCnAKwsBBTZJktcnnrHYXL\n" + + "h1oWr+qECTMw+O9i+KfUs3LXgzOuS102VbY+fLCqwFynLmyqVDE3b4Yu/5x68UCG\n" + + "/35qnVwnbYX8YrK6j+UdabAo/HnvZL7jk7pjRg1n3TIy+QE=\n" + + "=yFcL\n" + + "-----END PGP MESSAGE-----"; + + InputStream inputStream = new ByteArrayInputStream(message.getBytes(StandardCharsets.UTF_8)); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + assertThrows(WrongConsumingMethodException.class, + () -> ClearsignedMessageUtil.detachSignaturesFromInbandClearsignedMessage(inputStream, outputStream)); + } + + private String randomString(int maxWordLen, int wordCount) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < wordCount; i++) { + sb.append(randomWord(maxWordLen)).append(' '); + int n = random.nextInt(12); + if (n == 11) { + sb.append('\n'); + } + } + return sb.toString(); + } + + private String randomWord(int maxWordLen) { + int len = random.nextInt(maxWordLen); + char[] word = new char[len]; + for (int i = 0; i < word.length; i++) { + word[i] = alphabet.charAt(random.nextInt(alphabet.length())); + } + return new String(word); } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactoryTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactoryTest.java new file mode 100644 index 00000000..71fbf9be --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactoryTest.java @@ -0,0 +1,87 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPrivateKey; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; +import org.bouncycastle.openpgp.operator.bc.BcPublicKeyDataDecryptorFactory; +import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.EncryptionPurpose; +import org.pgpainless.encryption_signing.EncryptionOptions; +import org.pgpainless.encryption_signing.EncryptionStream; +import org.pgpainless.encryption_signing.ProducerOptions; +import org.pgpainless.key.SubkeyIdentifier; +import org.pgpainless.key.info.KeyRingInfo; +import org.pgpainless.key.protection.UnlockSecretKey; +import org.pgpainless.util.Passphrase; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class CustomPublicKeyDataDecryptorFactoryTest { + + @Test + public void testDecryptionWithEmulatedHardwareDecryptionCallback() + throws PGPException, IOException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + PGPSecretKeyRing secretKey = PGPainless.generateKeyRing().modernKeyRing("Alice"); + PGPPublicKeyRing cert = PGPainless.extractCertificate(secretKey); + KeyRingInfo info = PGPainless.inspectKeyRing(secretKey); + PGPPublicKey encryptionKey = info.getEncryptionSubkeys(EncryptionPurpose.ANY).get(0); + + // Encrypt a test message + String plaintext = "Hello, World!\n"; + ByteArrayOutputStream ciphertextOut = new ByteArrayOutputStream(); + EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() + .onOutputStream(ciphertextOut) + .withOptions(ProducerOptions.encrypt(EncryptionOptions.get() + .addRecipient(cert))); + encryptionStream.write(plaintext.getBytes(StandardCharsets.UTF_8)); + encryptionStream.close(); + + HardwareSecurity.DecryptionCallback hardwareDecryptionCallback = new HardwareSecurity.DecryptionCallback() { + @Override + public byte[] decryptSessionKey(long keyId, int keyAlgorithm, byte[] sessionKeyData, int pkeskVersion) + throws HardwareSecurity.HardwareSecurityException { + // Emulate hardware decryption. + try { + PGPSecretKey decryptionKey = secretKey.getSecretKey(encryptionKey.getKeyID()); + PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(decryptionKey, Passphrase.emptyPassphrase()); + PublicKeyDataDecryptorFactory internal = new BcPublicKeyDataDecryptorFactory(privateKey); + return internal.recoverSessionData(keyAlgorithm, new byte[][] {sessionKeyData}, pkeskVersion); + } catch (PGPException e) { + throw new HardwareSecurity.HardwareSecurityException(); + } + } + }; + + // Decrypt + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(new ByteArrayInputStream(ciphertextOut.toByteArray())) + .withOptions(ConsumerOptions.get() + .addCustomDecryptorFactory( + new HardwareSecurity.HardwareDataDecryptorFactory( + new SubkeyIdentifier(cert, encryptionKey.getKeyID()), + hardwareDecryptionCallback))); + + ByteArrayOutputStream decryptedOut = new ByteArrayOutputStream(); + Streams.pipeAll(decryptionStream, decryptedOut); + decryptionStream.close(); + + assertEquals(plaintext, decryptedOut.toString()); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptAndVerifyMessageTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptAndVerifyMessageTest.java index 3fa1ec4e..82796cb9 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptAndVerifyMessageTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptAndVerifyMessageTest.java @@ -6,6 +6,8 @@ package org.pgpainless.decryption_verification; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.ByteArrayInputStream; @@ -17,19 +19,23 @@ import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.CompressionAlgorithm; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; -import org.pgpainless.implementation.ImplementationFactory; +import org.pgpainless.exception.MissingDecryptionMethodException; import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.key.TestKeys; import org.pgpainless.key.util.KeyRingUtils; +import org.pgpainless.util.Passphrase; +import org.pgpainless.util.TestAllImplementations; public class DecryptAndVerifyMessageTest { // Don't use StandardCharsets.UTF8 because of Android API level. + @SuppressWarnings("CharsetObjectCanBeUsed") private static final Charset UTF8 = Charset.forName("UTF-8"); private PGPSecretKeyRing juliet; @@ -41,10 +47,9 @@ public class DecryptAndVerifyMessageTest { romeo = TestKeys.getRomeoSecretKeyRing(); } - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void decryptMessageAndVerifySignatureTest(ImplementationFactory implementationFactory) throws Exception { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void decryptMessageAndVerifySignatureTest() throws Exception { String encryptedMessage = TestKeys.MSG_SIGN_CRYPT_JULIET_JULIET; ConsumerOptions options = new ConsumerOptions() @@ -59,7 +64,7 @@ public class DecryptAndVerifyMessageTest { Streams.pipeAll(decryptor, toPlain); decryptor.close(); toPlain.close(); - OpenPgpMetadata metadata = decryptor.getResult(); + MessageMetadata metadata = decryptor.getMetadata(); byte[] expected = TestKeys.TEST_MESSAGE_01_PLAIN.getBytes(UTF8); byte[] actual = toPlain.toByteArray(); @@ -67,13 +72,87 @@ public class DecryptAndVerifyMessageTest { assertArrayEquals(expected, actual); assertTrue(metadata.isEncrypted()); - assertTrue(metadata.isSigned()); - assertTrue(metadata.isVerified()); + assertFalse(metadata.isUsingCleartextSignatureFramework()); + assertTrue(metadata.isVerifiedSigned()); assertEquals(CompressionAlgorithm.ZLIB, metadata.getCompressionAlgorithm()); - assertEquals(SymmetricKeyAlgorithm.AES_256, metadata.getSymmetricKeyAlgorithm()); - assertEquals(1, metadata.getSignatures().size()); + assertEquals(SymmetricKeyAlgorithm.AES_256, metadata.getEncryptionAlgorithm()); assertEquals(1, metadata.getVerifiedSignatures().size()); - assertTrue(metadata.containsVerifiedSignatureFrom(TestKeys.JULIET_FINGERPRINT)); + assertEquals(1, metadata.getVerifiedSignatures().size()); + assertTrue(metadata.isVerifiedSignedBy(TestKeys.JULIET_FINGERPRINT)); assertEquals(new SubkeyIdentifier(TestKeys.JULIET_FINGERPRINT), metadata.getDecryptionKey()); } + + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void decryptMessageAndReadBeyondEndTest() throws Exception { + final String encryptedMessage = TestKeys.MSG_SIGN_CRYPT_JULIET_JULIET; + + final ConsumerOptions options = new ConsumerOptions() + .addDecryptionKey(juliet) + .addVerificationCert(KeyRingUtils.publicKeyRingFrom(juliet)); + + try (DecryptionStream decryptor = PGPainless.decryptAndOrVerify() + .onInputStream(new ByteArrayInputStream(encryptedMessage.getBytes())) + .withOptions(options); + ByteArrayOutputStream toPlain = new ByteArrayOutputStream()) { + Streams.pipeAll(decryptor, toPlain); + assertEquals(-1, decryptor.read()); + } + } + + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void decryptMessageAndVerifySignatureByteByByteTest() throws Exception { + String encryptedMessage = TestKeys.MSG_SIGN_CRYPT_JULIET_JULIET; + + ConsumerOptions options = new ConsumerOptions() + .addDecryptionKey(juliet) + .addVerificationCert(KeyRingUtils.publicKeyRingFrom(juliet)); + + DecryptionStream decryptor = PGPainless.decryptAndOrVerify() + .onInputStream(new ByteArrayInputStream(encryptedMessage.getBytes())) + .withOptions(options); + + ByteArrayOutputStream toPlain = new ByteArrayOutputStream(); + int r; + while ((r = decryptor.read()) != -1) { + toPlain.write(r); + } + + decryptor.close(); + toPlain.close(); + MessageMetadata metadata = decryptor.getMetadata(); + + byte[] expected = TestKeys.TEST_MESSAGE_01_PLAIN.getBytes(UTF8); + byte[] actual = toPlain.toByteArray(); + + assertArrayEquals(expected, actual); + + assertTrue(metadata.isEncrypted()); + assertFalse(metadata.isUsingCleartextSignatureFramework()); + assertTrue(metadata.isVerifiedSigned()); + assertEquals(CompressionAlgorithm.ZLIB, metadata.getCompressionAlgorithm()); + assertEquals(SymmetricKeyAlgorithm.AES_256, metadata.getEncryptionAlgorithm()); + assertEquals(1, metadata.getVerifiedSignatures().size()); + assertEquals(1, metadata.getVerifiedSignatures().size()); + assertTrue(metadata.isVerifiedSignedBy(TestKeys.JULIET_FINGERPRINT)); + assertEquals(new SubkeyIdentifier(TestKeys.JULIET_FINGERPRINT), metadata.getDecryptionKey()); + } + + @Test + public void testDecryptMessageWithUnacceptableSymmetricAlgorithm() { + String ciphertext = "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "jA0EAQMCZv8glrLeXPhg0jgBpMN+E8dCuEDxJnSi8/e+HOKcdYQbgQh/MG4Kn7NK\n" + + "wRM5wNOFKn8jbsoC+JalzjwzMJSV+ZM1aQ==\n" + + "=9aCQ\n" + + "-----END PGP MESSAGE-----"; + ByteArrayInputStream ciphertextIn = new ByteArrayInputStream(ciphertext.getBytes()); + assertThrows(MissingDecryptionMethodException.class, + () -> PGPainless.decryptAndOrVerify() + .onInputStream(ciphertextIn) + .withOptions(ConsumerOptions.get() + .addMessagePassphrase(Passphrase.fromPassword("sw0rdf1sh")))); + } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptHiddenRecipientMessage.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptHiddenRecipientMessageTest.java similarity index 92% rename from pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptHiddenRecipientMessage.java rename to pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptHiddenRecipientMessageTest.java index 69d695d5..4eb7b203 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptHiddenRecipientMessage.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptHiddenRecipientMessageTest.java @@ -16,20 +16,19 @@ import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.util.io.Streams; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.EncryptionPurpose; -import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.key.info.KeyRingInfo; +import org.pgpainless.util.TestAllImplementations; -public class DecryptHiddenRecipientMessage { +public class DecryptHiddenRecipientMessageTest { - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void testDecryptionWithWildcardRecipient(ImplementationFactory implementationFactory) throws IOException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void testDecryptionWithWildcardRecipient() throws IOException, PGPException { String secretKeyAscii = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + "Comment: Bob's OpenPGP Transferable Secret Key\n" + "\n" + @@ -140,11 +139,12 @@ public class DecryptHiddenRecipientMessage { Streams.pipeAll(decryptionStream, out); decryptionStream.close(); - OpenPgpMetadata metadata = decryptionStream.getResult(); - assertEquals(0, metadata.getRecipientKeyIds().size()); + MessageMetadata metadata = decryptionStream.getMetadata(); + assertEquals(1, metadata.getRecipientKeyIds().size()); + assertEquals(0L, metadata.getRecipientKeyIds().get(0)); KeyRingInfo info = new KeyRingInfo(secretKeys); - List encryptionKeys = info.getEncryptionSubkeys(EncryptionPurpose.STORAGE_AND_COMMUNICATIONS); + List encryptionKeys = info.getEncryptionSubkeys(EncryptionPurpose.ANY); assertEquals(1, encryptionKeys.size()); assertEquals(new SubkeyIdentifier(secretKeys, encryptionKeys.get(0).getKeyID()), metadata.getDecryptionKey()); diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/IgnoreUnknownSignatureVersionsTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/IgnoreUnknownSignatureVersionsTest.java new file mode 100644 index 00000000..2b222c83 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/IgnoreUnknownSignatureVersionsTest.java @@ -0,0 +1,189 @@ +// SPDX-FileCopyrightText: 2021 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; + +/** + * Regression test for BC handling signatures of unknown version. + * This test makes sure that BC properly ignores unknown signature version. + */ +public class IgnoreUnknownSignatureVersionsTest { + + private static final String CERT = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "Comment: Bob's OpenPGP certificate\n" + + "\n" + + "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\n" + + "NEJd3XZRzaXZE2aAMQ==\n" + + "=NXei\n" + + "-----END PGP PUBLIC KEY BLOCK-----\n"; + private static final String message = "Hello World :)"; + + private static PGPPublicKeyRing cert; + static { + try { + cert = PGPainless.readKeyRing().publicKeyRing(CERT); + } catch (IOException e) { + fail("Cannot parse certificate.", e); + } + } + + @Test + public void baseCase() throws IOException, PGPException { + String BASE_CASE = "-----BEGIN PGP SIGNATURE-----\n" + + "\n" + + "wsE7BAABCgBvBYJhaVoTCRD7/MgqAV5zMEcUAAAAAAAeACBzYWx0QG5vdGF0aW9u\n" + + "cy5zZXF1b2lhLXBncC5vcmcQWMonyHF4Gcry0TOPKj/RQyhVoSI+1C31rXHHBcxI\n" + + "yxYhBNGmbhojsYLJmA94jPv8yCoBXnMwAAB7RwwAo+/P0/foJZp0/4yZWbBu/uNC\n" + + "0afvqv/tfcLVX6tZEmXkh6DfCtDwqX0vWrwAJuqtLUC8RvUDj7X4vi90/LhU2GUs\n" + + "+h+eZoQ3VwZ8jmfisQs7FUhbPOURZhRoS4UT8w7Le3pLodj5vAcaB6VvZP7SZ2Ls\n" + + "iNob9J0godpjlkGGWGqjWl0AO1kVvaNJTXpNhwpCRZyad8wUgri5QtrHWzRo4FBb\n" + + "JzrbJqWaS1FaqMCuPwcpq0KLsn4v6i4sD6Vy3HOaF29+avWNawnMLB/92csl4blq\n" + + "J20D2sKGUGcJpmLdiupnDdsHWU1jSpUvoUv+viE1RDqBFDQaNkUxdv5DhDOSRcwI\n" + + "a8Upx9lG8ol0uuDE4jieie4wuNq41jVRWJ55aJ46zO9QPifq59SMcRj8uN8byRKz\n" + + "Wdg/ngldiePCjg2RQztgb6Hsut7xeYYhuAYQ8m1+vLCnS8tRnCqhAqxdW51uSfIq\n" + + "xKukH+bioF/+baqBu1AlXmNVou1uiXJaDzZ6wQfBwsE7BAABCgBvBYJhaVoTCRD7\n" + + "/MgqAV5zMEcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmcQ\n" + + "WMonyHF4Gcry0TOPKj/RQyhVoSI+1C31rXHHBcxIyxYhBNGmbhojsYLJmA94jPv8\n" + + "yCoBXnMwAAB7RwwAo+/P0/foJZp0/4yZWbBu/uNC0afvqv/tfcLVX6tZEmXkh6Df\n" + + "CtDwqX0vWrwAJuqtLUC8RvUDj7X4vi90/LhU2GUs+h+eZoQ3VwZ8jmfisQs7FUhb\n" + + "POURZhRoS4UT8w7Le3pLodj5vAcaB6VvZP7SZ2LsiNob9J0godpjlkGGWGqjWl0A\n" + + "O1kVvaNJTXpNhwpCRZyad8wUgri5QtrHWzRo4FBbJzrbJqWaS1FaqMCuPwcpq0KL\n" + + "sn4v6i4sD6Vy3HOaF29+avWNawnMLB/92csl4blqJ20D2sKGUGcJpmLdiupnDdsH\n" + + "WU1jSpUvoUv+viE1RDqBFDQaNkUxdv5DhDOSRcwIa8Upx9lG8ol0uuDE4jieie4w\n" + + "uNq41jVRWJ55aJ46zO9QPifq59SMcRj8uN8byRKzWdg/ngldiePCjg2RQztgb6Hs\n" + + "ut7xeYYhuAYQ8m1+vLCnS8tRnCqhAqxdW51uSfIqxKukH+bioF/+baqBu1AlXmNV\n" + + "ou1uiXJaDzZ6wQfB\n" + + "=uHRc\n" + + "-----END PGP SIGNATURE-----\n"; + MessageMetadata metadata = verifySignature(cert, BASE_CASE); + + assertTrue(metadata.isVerifiedSigned()); + } + + @Test + public void detached_SIG4_SIG23() throws PGPException, IOException { + String SIG4SIG23 = "-----BEGIN PGP SIGNATURE-----\n" + + "\n" + + "wsE7BAABCgBvBYJhaVoTCRD7/MgqAV5zMEcUAAAAAAAeACBzYWx0QG5vdGF0aW9u\n" + + "cy5zZXF1b2lhLXBncC5vcmcQWMonyHF4Gcry0TOPKj/RQyhVoSI+1C31rXHHBcxI\n" + + "yxYhBNGmbhojsYLJmA94jPv8yCoBXnMwAAB7RwwAo+/P0/foJZp0/4yZWbBu/uNC\n" + + "0afvqv/tfcLVX6tZEmXkh6DfCtDwqX0vWrwAJuqtLUC8RvUDj7X4vi90/LhU2GUs\n" + + "+h+eZoQ3VwZ8jmfisQs7FUhbPOURZhRoS4UT8w7Le3pLodj5vAcaB6VvZP7SZ2Ls\n" + + "iNob9J0godpjlkGGWGqjWl0AO1kVvaNJTXpNhwpCRZyad8wUgri5QtrHWzRo4FBb\n" + + "JzrbJqWaS1FaqMCuPwcpq0KLsn4v6i4sD6Vy3HOaF29+avWNawnMLB/92csl4blq\n" + + "J20D2sKGUGcJpmLdiupnDdsHWU1jSpUvoUv+viE1RDqBFDQaNkUxdv5DhDOSRcwI\n" + + "a8Upx9lG8ol0uuDE4jieie4wuNq41jVRWJ55aJ46zO9QPifq59SMcRj8uN8byRKz\n" + + "Wdg/ngldiePCjg2RQztgb6Hsut7xeYYhuAYQ8m1+vLCnS8tRnCqhAqxdW51uSfIq\n" + + "xKukH+bioF/+baqBu1AlXmNVou1uiXJaDzZ6wQfBwsE7FwABCgBvBYJhaVoTCRD7\n" + + "/MgqAV5zMEcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmcQ\n" + + "WMonyHF4Gcry0TOPKj/RQyhVoSI+1C31rXHHBcxIyxYhBNGmbhojsYLJmA94jPv8\n" + + "yCoBXnMwAAB7RwwAo+/P0/foJZp0/4yZWbBu/uNC0afvqv/tfcLVX6tZEmXkh6Df\n" + + "CtDwqX0vWrwAJuqtLUC8RvUDj7X4vi90/LhU2GUs+h+eZoQ3VwZ8jmfisQs7FUhb\n" + + "POURZhRoS4UT8w7Le3pLodj5vAcaB6VvZP7SZ2LsiNob9J0godpjlkGGWGqjWl0A\n" + + "O1kVvaNJTXpNhwpCRZyad8wUgri5QtrHWzRo4FBbJzrbJqWaS1FaqMCuPwcpq0KL\n" + + "sn4v6i4sD6Vy3HOaF29+avWNawnMLB/92csl4blqJ20D2sKGUGcJpmLdiupnDdsH\n" + + "WU1jSpUvoUv+viE1RDqBFDQaNkUxdv5DhDOSRcwIa8Upx9lG8ol0uuDE4jieie4w\n" + + "uNq41jVRWJ55aJ46zO9QPifq59SMcRj8uN8byRKzWdg/ngldiePCjg2RQztgb6Hs\n" + + "ut7xeYYhuAYQ8m1+vLCnS8tRnCqhAqxdW51uSfIqxKukH+bioF/+baqBu1AlXmNV\n" + + "ou1uiXJaDzZ6wQfB\n" + + "=/JL1\n" + + "-----END PGP SIGNATURE-----\n"; + MessageMetadata metadata = verifySignature(cert, SIG4SIG23); + + assertTrue(metadata.isVerifiedSigned()); + } + + @Test + public void detached_SIG23_SIG4() throws PGPException, IOException { + String SIG23SIG4 = "-----BEGIN PGP SIGNATURE-----\n" + + "\n" + + "wsE7FwABCgBvBYJhaVoTCRD7/MgqAV5zMEcUAAAAAAAeACBzYWx0QG5vdGF0aW9u\n" + + "cy5zZXF1b2lhLXBncC5vcmcQWMonyHF4Gcry0TOPKj/RQyhVoSI+1C31rXHHBcxI\n" + + "yxYhBNGmbhojsYLJmA94jPv8yCoBXnMwAAB7RwwAo+/P0/foJZp0/4yZWbBu/uNC\n" + + "0afvqv/tfcLVX6tZEmXkh6DfCtDwqX0vWrwAJuqtLUC8RvUDj7X4vi90/LhU2GUs\n" + + "+h+eZoQ3VwZ8jmfisQs7FUhbPOURZhRoS4UT8w7Le3pLodj5vAcaB6VvZP7SZ2Ls\n" + + "iNob9J0godpjlkGGWGqjWl0AO1kVvaNJTXpNhwpCRZyad8wUgri5QtrHWzRo4FBb\n" + + "JzrbJqWaS1FaqMCuPwcpq0KLsn4v6i4sD6Vy3HOaF29+avWNawnMLB/92csl4blq\n" + + "J20D2sKGUGcJpmLdiupnDdsHWU1jSpUvoUv+viE1RDqBFDQaNkUxdv5DhDOSRcwI\n" + + "a8Upx9lG8ol0uuDE4jieie4wuNq41jVRWJ55aJ46zO9QPifq59SMcRj8uN8byRKz\n" + + "Wdg/ngldiePCjg2RQztgb6Hsut7xeYYhuAYQ8m1+vLCnS8tRnCqhAqxdW51uSfIq\n" + + "xKukH+bioF/+baqBu1AlXmNVou1uiXJaDzZ6wQfBwsE7BAABCgBvBYJhaVoTCRD7\n" + + "/MgqAV5zMEcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmcQ\n" + + "WMonyHF4Gcry0TOPKj/RQyhVoSI+1C31rXHHBcxIyxYhBNGmbhojsYLJmA94jPv8\n" + + "yCoBXnMwAAB7RwwAo+/P0/foJZp0/4yZWbBu/uNC0afvqv/tfcLVX6tZEmXkh6Df\n" + + "CtDwqX0vWrwAJuqtLUC8RvUDj7X4vi90/LhU2GUs+h+eZoQ3VwZ8jmfisQs7FUhb\n" + + "POURZhRoS4UT8w7Le3pLodj5vAcaB6VvZP7SZ2LsiNob9J0godpjlkGGWGqjWl0A\n" + + "O1kVvaNJTXpNhwpCRZyad8wUgri5QtrHWzRo4FBbJzrbJqWaS1FaqMCuPwcpq0KL\n" + + "sn4v6i4sD6Vy3HOaF29+avWNawnMLB/92csl4blqJ20D2sKGUGcJpmLdiupnDdsH\n" + + "WU1jSpUvoUv+viE1RDqBFDQaNkUxdv5DhDOSRcwIa8Upx9lG8ol0uuDE4jieie4w\n" + + "uNq41jVRWJ55aJ46zO9QPifq59SMcRj8uN8byRKzWdg/ngldiePCjg2RQztgb6Hs\n" + + "ut7xeYYhuAYQ8m1+vLCnS8tRnCqhAqxdW51uSfIqxKukH+bioF/+baqBu1AlXmNV\n" + + "ou1uiXJaDzZ6wQfB\n" + + "=Yc8d\n" + + "-----END PGP SIGNATURE-----\n"; + MessageMetadata metadata = verifySignature(cert, SIG23SIG4); + + assertTrue(metadata.isVerifiedSigned()); + } + + private MessageMetadata verifySignature(PGPPublicKeyRing cert, String BASE_CASE) throws PGPException, IOException { + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify().onInputStream(new ByteArrayInputStream(message.getBytes(StandardCharsets.UTF_8))) + .withOptions(new ConsumerOptions() + .addVerificationCert(cert) + .addVerificationOfDetachedSignatures(new ByteArrayInputStream(BASE_CASE.getBytes(StandardCharsets.UTF_8)))); + + Streams.drain(decryptionStream); + decryptionStream.close(); + + return decryptionStream.getMetadata(); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MessageInspectorTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MessageInspectorTest.java index 2f08a432..b7382360 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MessageInspectorTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MessageInspectorTest.java @@ -8,9 +8,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; -import java.io.ByteArrayInputStream; import java.io.IOException; -import java.nio.charset.StandardCharsets; import org.bouncycastle.openpgp.PGPException; import org.junit.jupiter.api.Test; @@ -29,14 +27,13 @@ public class MessageInspectorTest { "=IICf\n" + "-----END PGP MESSAGE-----\n"; - MessageInspector.EncryptionInfo info = MessageInspector.determineEncryptionInfoForMessage( - new ByteArrayInputStream(message.getBytes(StandardCharsets.UTF_8))); + MessageInspector.EncryptionInfo info = MessageInspector.determineEncryptionInfoForMessage(message); assertFalse(info.isPassphraseEncrypted()); assertFalse(info.isSignedOnly()); assertTrue(info.isEncrypted()); assertEquals(1, info.getKeyIds().size()); - assertEquals(KeyIdUtil.fromLongKeyId("4766F6B9D5F21EB6"), info.getKeyIds().get(0)); + assertEquals(KeyIdUtil.fromLongKeyId("4766F6B9D5F21EB6"), (long) info.getKeyIds().get(0)); } @Test @@ -55,7 +52,7 @@ public class MessageInspectorTest { "=z6e0\n" + "-----END PGP MESSAGE-----"; - MessageInspector.EncryptionInfo info = MessageInspector.determineEncryptionInfoForMessage(new ByteArrayInputStream(message.getBytes(StandardCharsets.UTF_8))); + MessageInspector.EncryptionInfo info = MessageInspector.determineEncryptionInfoForMessage(message); assertTrue(info.isEncrypted()); assertTrue(info.isPassphraseEncrypted()); @@ -77,7 +74,7 @@ public class MessageInspectorTest { "=nt5n\n" + "-----END PGP MESSAGE-----"; - MessageInspector.EncryptionInfo info = MessageInspector.determineEncryptionInfoForMessage(new ByteArrayInputStream(message.getBytes(StandardCharsets.UTF_8))); + MessageInspector.EncryptionInfo info = MessageInspector.determineEncryptionInfoForMessage(message); assertTrue(info.isSignedOnly()); @@ -100,7 +97,7 @@ public class MessageInspectorTest { "KK0Ymg5GrsBTEGFm4jb1p+V85PPhsIioX3np/N3fkIfxFguTGZza33/GHy61+DTy\n" + "=SZU6\n" + "-----END PGP MESSAGE-----"; - MessageInspector.EncryptionInfo info = MessageInspector.determineEncryptionInfoForMessage(new ByteArrayInputStream(message.getBytes(StandardCharsets.UTF_8))); + MessageInspector.EncryptionInfo info = MessageInspector.determineEncryptionInfoForMessage(message); // Message is encrypted, so we cannot determine if it is signed or not. // It is not signed only @@ -110,4 +107,39 @@ public class MessageInspectorTest { assertTrue(info.isPassphraseEncrypted()); assertEquals(1, info.getKeyIds().size()); } + + @Test + public void testPlaintextMessage() throws IOException, PGPException { + String message = "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "Comment: Literal Data\n" + + "\n" + + "yyl0CF9DT05TT0xFYXlXgUp1c3Qgc29tZSB1bmVuY3J5cHRlZCBkYXRhLg==\n" + + "=jVNT\n" + + "-----END PGP MESSAGE-----"; + + MessageInspector.EncryptionInfo info = MessageInspector.determineEncryptionInfoForMessage(message); + assertFalse(info.isEncrypted()); + assertFalse(info.isSignedOnly()); + assertFalse(info.isPassphraseEncrypted()); + assertTrue(info.getKeyIds().isEmpty()); + } + + @Test + public void testCompressedPlaintextMessage() throws IOException, PGPException { + String message = "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "Comment: Compressed Literal Data\n" + + "\n" + + "owE7HVDCEe/s7xfs7+OaWBmxJDw1L08hIzOvJLVIwS0nMzU9NQ9Op0FoHRgDQ0Fe\n" + + "YnKGHgA=\n" + + "=jw3E\n" + + "-----END PGP MESSAGE-----"; + + MessageInspector.EncryptionInfo info = MessageInspector.determineEncryptionInfoForMessage(message); + assertFalse(info.isEncrypted()); + assertFalse(info.isSignedOnly()); + assertFalse(info.isPassphraseEncrypted()); + assertTrue(info.getKeyIds().isEmpty()); + } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MessageMetadataTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MessageMetadataTest.java new file mode 100644 index 00000000..7c443829 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MessageMetadataTest.java @@ -0,0 +1,84 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification; + +import org.junit.JUtils; +import org.junit.jupiter.api.Test; +import org.pgpainless.algorithm.CompressionAlgorithm; +import org.pgpainless.algorithm.StreamEncoding; +import org.pgpainless.algorithm.SymmetricKeyAlgorithm; +import org.pgpainless.util.DateUtil; + +import java.util.Date; +import java.util.Iterator; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class MessageMetadataTest { + + @Test + public void processTestMessage_COMP_ENC_ENC_LIT() { + // Note: COMP of ENC does not make sense, since ENC is indistinguishable from randomness + // and randomness cannot be compressed. + // For the sake of testing though, this is okay. + MessageMetadata.Message message = new MessageMetadata.Message(); + + MessageMetadata.CompressedData compressedData = new MessageMetadata.CompressedData(CompressionAlgorithm.ZIP, message.getDepth() + 1); + MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData(SymmetricKeyAlgorithm.AES_128, compressedData.getDepth() + 1); + MessageMetadata.EncryptedData encryptedData1 = new MessageMetadata.EncryptedData(SymmetricKeyAlgorithm.AES_256, encryptedData.getDepth() + 1); + MessageMetadata.LiteralData literalData = new MessageMetadata.LiteralData(); + + message.setChild(compressedData); + compressedData.setChild(encryptedData); + encryptedData.setChild(encryptedData1); + encryptedData1.setChild(literalData); + + MessageMetadata metadata = new MessageMetadata(message); + + // Check encryption algs + assertEquals(SymmetricKeyAlgorithm.AES_128, metadata.getEncryptionAlgorithm(), "getEncryptionAlgorithm() returns alg of outermost EncryptedData"); + Iterator encryptionAlgs = metadata.getEncryptionAlgorithms(); + assertTrue(encryptionAlgs.hasNext(), "There is at least one EncryptedData child"); + assertTrue(encryptionAlgs.hasNext(), "The child is still there"); + assertEquals(SymmetricKeyAlgorithm.AES_128, encryptionAlgs.next(), "The first algo is AES128"); + assertTrue(encryptionAlgs.hasNext(), "There is another EncryptedData"); + assertTrue(encryptionAlgs.hasNext(), "There is *still* another EncryptedData"); + assertEquals(SymmetricKeyAlgorithm.AES_256, encryptionAlgs.next(), "The second algo is AES256"); + assertFalse(encryptionAlgs.hasNext(), "There is no more EncryptedData"); + assertFalse(encryptionAlgs.hasNext(), "There *still* is no more EncryptedData"); + + assertEquals(CompressionAlgorithm.ZIP, metadata.getCompressionAlgorithm(), "getCompressionAlgorithm() returns alg of outermost CompressedData"); + Iterator compAlgs = metadata.getCompressionAlgorithms(); + assertTrue(compAlgs.hasNext()); + assertTrue(compAlgs.hasNext()); + assertEquals(CompressionAlgorithm.ZIP, compAlgs.next()); + assertFalse(compAlgs.hasNext()); + assertFalse(compAlgs.hasNext()); + + assertEquals("", metadata.getFilename()); + JUtils.assertDateEquals(new Date(0L), metadata.getModificationDate()); + assertEquals(StreamEncoding.BINARY, metadata.getLiteralDataEncoding()); + } + + @Test + public void testProcessLiteralDataMessage() { + MessageMetadata.LiteralData literalData = new MessageMetadata.LiteralData( + "collateral_murder.zip", + DateUtil.parseUTCDate("2010-04-05 10:12:03 UTC"), + StreamEncoding.BINARY); + MessageMetadata.Message message = new MessageMetadata.Message(); + message.setChild(literalData); + + MessageMetadata metadata = new MessageMetadata(message); + assertNull(metadata.getCompressionAlgorithm()); + assertNull(metadata.getEncryptionAlgorithm()); + assertEquals("collateral_murder.zip", metadata.getFilename()); + assertEquals(DateUtil.parseUTCDate("2010-04-05 10:12:03 UTC"), metadata.getModificationDate()); + assertEquals(StreamEncoding.BINARY, metadata.getLiteralDataEncoding()); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MissingPassphraseForDecryptionTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MissingPassphraseForDecryptionTest.java index e7b36875..42562713 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MissingPassphraseForDecryptionTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MissingPassphraseForDecryptionTest.java @@ -7,7 +7,6 @@ package org.pgpainless.decryption_verification; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @@ -18,7 +17,6 @@ import java.nio.charset.StandardCharsets; import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; import java.util.List; -import javax.annotation.Nullable; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKey; @@ -41,7 +39,7 @@ import org.pgpainless.util.Passphrase; public class MissingPassphraseForDecryptionTest { - private String passphrase = "dragon123"; + private final String passphrase = "dragon123"; private PGPSecretKeyRing secretKeys; private byte[] message; @@ -60,35 +58,10 @@ public class MissingPassphraseForDecryptionTest { message = out.toByteArray(); } - @Test - public void invalidPostponedKeysStrategyTest() { - SecretKeyPassphraseProvider callback = new SecretKeyPassphraseProvider() { - @Nullable - @Override - public Passphrase getPassphraseFor(Long keyId) { - fail("MUST NOT get called in if postponed key strategy is invalid."); - return null; - } - - @Override - public boolean hasPassphrase(Long keyId) { - return true; - } - }; - ConsumerOptions options = new ConsumerOptions() - .setMissingKeyPassphraseStrategy(null) // illegal - .addDecryptionKey(secretKeys, SecretKeyRingProtector.defaultSecretKeyRingProtector(callback)); - - assertThrows(IllegalStateException.class, () -> PGPainless.decryptAndOrVerify() - .onInputStream(new ByteArrayInputStream(message)) - .withOptions(options)); - } - @Test public void interactiveStrategy() throws PGPException, IOException { // interactive callback SecretKeyPassphraseProvider callback = new SecretKeyPassphraseProvider() { - @Nullable @Override public Passphrase getPassphraseFor(Long keyId) { // is called in interactive mode @@ -118,10 +91,9 @@ public class MissingPassphraseForDecryptionTest { @Test public void throwExceptionStrategy() throws PGPException, IOException { KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); - List encryptionKeys = info.getEncryptionSubkeys(EncryptionPurpose.STORAGE_AND_COMMUNICATIONS); + List encryptionKeys = info.getEncryptionSubkeys(EncryptionPurpose.ANY); SecretKeyPassphraseProvider callback = new SecretKeyPassphraseProvider() { - @Nullable @Override public Passphrase getPassphraseFor(Long keyId) { fail("MUST NOT get called in non-interactive mode."); diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/ModificationDetectionTests.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/ModificationDetectionTests.java index 2a2a0844..59021f95 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/ModificationDetectionTests.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/ModificationDetectionTests.java @@ -4,6 +4,7 @@ package org.pgpainless.decryption_verification; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import java.io.ByteArrayInputStream; @@ -18,14 +19,13 @@ import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; import org.bouncycastle.util.io.Streams; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; import org.pgpainless.PGPainless; import org.pgpainless.exception.MessageNotIntegrityProtectedException; import org.pgpainless.exception.ModificationDetectionException; -import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.util.TestAllImplementations; import org.pgpainless.util.Passphrase; public class ModificationDetectionTests { @@ -205,14 +205,12 @@ public class ModificationDetectionTests { /** * Messages containing a missing MDC shall fail to decrypt. - * @param implementationFactory implementation factory * @throws IOException in case of an io-error * @throws PGPException in case of a pgp error */ - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void testMissingMDCThrowsByDefault(ImplementationFactory implementationFactory) throws IOException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void testMissingMDCThrowsByDefault() throws IOException, PGPException { PGPSecretKeyRingCollection secretKeyRings = getDecryptionKey(); @@ -230,10 +228,9 @@ public class ModificationDetectionTests { }); } - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void testTamperedCiphertextThrows(ImplementationFactory implementationFactory) throws IOException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void testTamperedCiphertextThrows() throws IOException, PGPException { ByteArrayInputStream in = new ByteArrayInputStream(MESSAGE_TAMPERED_CIPHERTEXT.getBytes(StandardCharsets.UTF_8)); DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() .onInputStream(in) @@ -242,14 +239,15 @@ public class ModificationDetectionTests { ); ByteArrayOutputStream out = new ByteArrayOutputStream(); - Streams.pipeAll(decryptionStream, out); - assertThrows(ModificationDetectionException.class, decryptionStream::close); + assertThrows(ModificationDetectionException.class, () -> { + Streams.pipeAll(decryptionStream, out); + decryptionStream.close(); + }); } - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void testIgnoreTamperedCiphertext(ImplementationFactory implementationFactory) throws IOException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void testIgnoreTamperedCiphertext() throws IOException, PGPException { ByteArrayInputStream in = new ByteArrayInputStream(MESSAGE_TAMPERED_CIPHERTEXT.getBytes(StandardCharsets.UTF_8)); DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() .onInputStream(in) @@ -263,10 +261,9 @@ public class ModificationDetectionTests { decryptionStream.close(); } - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void testTamperedMDCThrowsByDefault(ImplementationFactory implementationFactory) throws IOException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void testTamperedMDCThrowsByDefault() throws IOException, PGPException { ByteArrayInputStream in = new ByteArrayInputStream(MESSAGE_TAMPERED_MDC.getBytes(StandardCharsets.UTF_8)); DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() .onInputStream(in) @@ -275,14 +272,15 @@ public class ModificationDetectionTests { ); ByteArrayOutputStream out = new ByteArrayOutputStream(); - Streams.pipeAll(decryptionStream, out); - assertThrows(ModificationDetectionException.class, decryptionStream::close); + assertThrows(ModificationDetectionException.class, () -> { + Streams.pipeAll(decryptionStream, out); + decryptionStream.close(); + }); } - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void testIgnoreTamperedMDC(ImplementationFactory implementationFactory) throws IOException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void testIgnoreTamperedMDC() throws IOException, PGPException { ByteArrayInputStream in = new ByteArrayInputStream(MESSAGE_TAMPERED_MDC.getBytes(StandardCharsets.UTF_8)); DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() .onInputStream(in) @@ -295,10 +293,9 @@ public class ModificationDetectionTests { Streams.pipeAll(decryptionStream, out); } - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void testTruncatedMDCThrows(ImplementationFactory implementationFactory) throws IOException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void testTruncatedMDCThrows() throws IOException, PGPException { ByteArrayInputStream in = new ByteArrayInputStream(MESSAGE_TRUNCATED_MDC.getBytes(StandardCharsets.UTF_8)); DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() .onInputStream(in) @@ -310,10 +307,9 @@ public class ModificationDetectionTests { assertThrows(EOFException.class, () -> Streams.pipeAll(decryptionStream, out)); } - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void testMDCWithBadCTBThrows(ImplementationFactory implementationFactory) throws IOException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void testMDCWithBadCTBThrows() throws IOException, PGPException { ByteArrayInputStream in = new ByteArrayInputStream(MESSAGE_MDC_WITH_BAD_CTB.getBytes(StandardCharsets.UTF_8)); DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() .onInputStream(in) @@ -322,14 +318,15 @@ public class ModificationDetectionTests { ); ByteArrayOutputStream out = new ByteArrayOutputStream(); - Streams.pipeAll(decryptionStream, out); - assertThrows(ModificationDetectionException.class, decryptionStream::close); + assertThrows(ModificationDetectionException.class, () -> { + Streams.pipeAll(decryptionStream, out); + decryptionStream.close(); + }); } - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void testIgnoreMDCWithBadCTB(ImplementationFactory implementationFactory) throws IOException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void testIgnoreMDCWithBadCTB() throws IOException, PGPException { ByteArrayInputStream in = new ByteArrayInputStream(MESSAGE_MDC_WITH_BAD_CTB.getBytes(StandardCharsets.UTF_8)); DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() .onInputStream(in) @@ -343,10 +340,9 @@ public class ModificationDetectionTests { decryptionStream.close(); } - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void testMDCWithBadLengthThrows(ImplementationFactory implementationFactory) throws IOException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void testMDCWithBadLengthThrows() throws IOException, PGPException { ByteArrayInputStream in = new ByteArrayInputStream(MESSAGE_MDC_WITH_BAD_LENGTH.getBytes(StandardCharsets.UTF_8)); DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() .onInputStream(in) @@ -355,14 +351,15 @@ public class ModificationDetectionTests { ); ByteArrayOutputStream out = new ByteArrayOutputStream(); - Streams.pipeAll(decryptionStream, out); - assertThrows(ModificationDetectionException.class, decryptionStream::close); + assertThrows(ModificationDetectionException.class, () -> { + Streams.pipeAll(decryptionStream, out); + decryptionStream.close(); + }); } - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void testIgnoreMDCWithBadLength(ImplementationFactory implementationFactory) throws IOException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void testIgnoreMDCWithBadLength() throws IOException, PGPException { ByteArrayInputStream in = new ByteArrayInputStream(MESSAGE_MDC_WITH_BAD_LENGTH.getBytes(StandardCharsets.UTF_8)); DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() .onInputStream(in) @@ -376,7 +373,8 @@ public class ModificationDetectionTests { decryptionStream.close(); } - @Test + @TestTemplate + @ExtendWith(TestAllImplementations.class) public void decryptMessageWithSEDPacket() throws IOException, PGPException { Passphrase passphrase = Passphrase.fromPassword("flowcrypt compatibility tests"); String key = "-----BEGIN PGP PRIVATE KEY BLOCK-----\r\n" + @@ -537,11 +535,26 @@ public class ModificationDetectionTests { assertThrows(MessageNotIntegrityProtectedException.class, () -> PGPainless.decryptAndOrVerify() .onInputStream(new ByteArrayInputStream(ciphertext.getBytes(StandardCharsets.UTF_8))) .withOptions(new ConsumerOptions().addDecryptionKey(secretKeyRing, - SecretKeyRingProtector.unlockAllKeysWith(passphrase, secretKeyRing))) + SecretKeyRingProtector.unlockAnyKeyWith(passphrase))) ); + + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(new ByteArrayInputStream(ciphertext.getBytes(StandardCharsets.UTF_8))) + .withOptions(ConsumerOptions.get().addDecryptionKey(secretKeyRing, + SecretKeyRingProtector.unlockAnyKeyWith(passphrase)) + .setIgnoreMDCErrors(true)); + ByteArrayOutputStream plaintext = new ByteArrayOutputStream(); + Streams.pipeAll(decryptionStream, plaintext); + decryptionStream.close(); + + assertEquals("As stated in subject\r\n" + + "\r\n" + + "Shall not decrypt automatically\r\n" + + "\r\n" + + "Has to show a warning\r\n", plaintext.toString()); } - private PGPSecretKeyRingCollection getDecryptionKey() throws IOException, PGPException { + private PGPSecretKeyRingCollection getDecryptionKey() throws IOException { PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(keyAscii); return new PGPSecretKeyRingCollection(Collections.singletonList(secretKeys)); } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpInputStreamTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpInputStreamTest.java new file mode 100644 index 00000000..d731fb87 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpInputStreamTest.java @@ -0,0 +1,761 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.util.Random; + +import org.bouncycastle.bcpg.ArmoredInputStream; +import org.bouncycastle.bcpg.CompressionAlgorithmTags; +import org.bouncycastle.openpgp.PGPCompressedDataGenerator; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.CompressionAlgorithm; +import org.pgpainless.encryption_signing.EncryptionStream; +import org.pgpainless.encryption_signing.ProducerOptions; +import org.pgpainless.encryption_signing.SigningOptions; +import org.pgpainless.key.protection.SecretKeyRingProtector; + +public class OpenPgpInputStreamTest { + + private static final Random RANDOM = new Random(); + + @Test + public void randomBytesDoNotContainOpenPgpData() throws IOException { + byte[] randomBytes = new byte[1000000]; + RANDOM.nextBytes(randomBytes); + ByteArrayInputStream randomIn = new ByteArrayInputStream(randomBytes); + + OpenPgpInputStream openPgpInputStream = new OpenPgpInputStream(randomIn); + assertFalse(openPgpInputStream.isAsciiArmored()); + assertFalse(openPgpInputStream.isLikelyOpenPgpMessage()); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Streams.pipeAll(openPgpInputStream, out); + byte[] outBytes = out.toByteArray(); + + assertArrayEquals(randomBytes, outBytes); + } + + @Test + public void largeCompressedDataIsBinaryOpenPgp() throws IOException { + // Since we are compressing RANDOM data, the output will likely be roughly the same size + // So we very likely will end up with data larger than the MAX_BUFFER_SIZE + byte[] randomBytes = new byte[OpenPgpInputStream.MAX_BUFFER_SIZE * 10]; + RANDOM.nextBytes(randomBytes); + + ByteArrayOutputStream compressedDataPacket = new ByteArrayOutputStream(); + PGPCompressedDataGenerator compressedDataGenerator = new PGPCompressedDataGenerator(CompressionAlgorithmTags.ZIP); + OutputStream compressor = compressedDataGenerator.open(compressedDataPacket); + compressor.write(randomBytes); + compressor.close(); + + OpenPgpInputStream inputStream = new OpenPgpInputStream(new ByteArrayInputStream(compressedDataPacket.toByteArray())); + assertFalse(inputStream.isAsciiArmored()); + assertFalse(inputStream.isNonOpenPgp()); + assertTrue(inputStream.isBinaryOpenPgp()); + assertTrue(inputStream.isLikelyOpenPgpMessage()); + } + + @Test + public void shortAsciiArmoredMessageIsAsciiArmored() throws IOException { + String asciiArmoredMessage = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "wcDMAwAAAAAAAAAAAQv9FBhYmbkqLBVrhUUPouXTiXJ/ElyDknSW0xTDgofFbIZ5\n" + + "9ABYrYHaDEUAupwYzh5H8xNiL70/RdI0cMv7k2Rqlug/W4f0Mz+wYJ4xN24NzRQ5\n" + + "BqlsTIlXwJI0N4Rj7KSBfVhSHYEm0EtA4qx8ylL3vJfAH1AH7bBLjSzkDYE7dvu8\n" + + "2/PigN2c0tQ+AG4O+QV8zgJpc0tE2bh0h1eiXarhOZZSNjJKqmYZ4PlhgdiQBRs7\n" + + "a7EgkdNYMUTCbBiEpyQiiorDIxqmiaQVJjoCmSiSMCxvae9ozue6x1FvFyZWEPdV\n" + + "Lp8pSnuZwQt7jAw/Qm3u1ogyNdQaoXF/pDuwJEf0ufYwMsI7wDUVUJiRL23BGDOB\n" + + "h2YbFu7TWz63wkwjTs8bfeQ8JPmWXTG75Z95sjaiMloGhKwhYem8XPWAmh6xLWfF\n" + + "TgYU/AgKTgBvb/WugSLpi1zSOjkET3IY00vjvCzfwxxojJd/vfaSdOQX2EbADwgm\n" + + "KAmdO0Q9+BRuBDNPAEH/0j8BuiicOrrHRd0c9T4ku9u1vvxGJCMwiKPj9TGlxxpw\n" + + "C5uUVzvOSzGKfZ5ZH4SToaMhbYW37UXtA7URW1zF86c=\n" + + "=Yz3x\n" + + "-----END PGP MESSAGE-----"; + + ByteArrayInputStream asciiIn = new ByteArrayInputStream(asciiArmoredMessage.getBytes(StandardCharsets.UTF_8)); + OpenPgpInputStream openPgpInputStream = new OpenPgpInputStream(asciiIn); + + assertTrue(openPgpInputStream.isAsciiArmored()); + assertFalse(openPgpInputStream.isNonOpenPgp()); + assertFalse(openPgpInputStream.isBinaryOpenPgp()); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Streams.pipeAll(openPgpInputStream, out); + + assertArrayEquals(asciiArmoredMessage.getBytes(StandardCharsets.UTF_8), out.toByteArray()); + } + + String longAsciiArmoredMessage = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "Comment: 7F91 16FE A90A 5983 936C 7CFA A027 DB2F 3E1E 118A\n" + + "Comment: Paul Schaub \n" + + "Comment: Paul Schaub \n" + + "Comment: Paul Schaub \n" + + "\n" + + "xsFNBFfz1ucBEADXSvUjnOWSzgW5hXki1xUpGv7vacT8XqqGbO9Z32P3eFxa4E9J\n" + + "vveJmx+voxRWpleZ/L6XCYYmCKnagjF0fMxFD1Zxicp5tzbruC1cm/Els0IJVjFV\n" + + "RLke3SegTHxHncA8+BYn2k/VnTKwDXzP0ZLyc7mUbDl8CCtWGGUkXpaa7WyZIA/q\n" + + "mvUqh7671Vr4vJlq0kFbUibsFblZjk9uydHvvqaVpmBzbr/gWDyirHXwPl5lCnWp\n" + + "ORjT7tc8hjyt+dxpmnGdqlDIcqUjdCWoN6NxffLtKz/XpJ+dBvA8rXT/QaPSaVCG\n" + + "o0DbgybvRF1HvX30udx4FF9fFsVAbYP1mvZx4fHy+Z1rJJhODZv1YpH7YY1bmG02\n" + + "vfFkwpW4AyAdsONA+n/XdMCsA006/pljNd3GxjcqB5D6BhpdUvcgUslkuELsVYWb\n" + + "EyhxKzzJvZNjQ/iHsaThooy9SFHc71PgYdyEL/WzoGr421GwpCL6BuE0rlumgaTm\n" + + "joU/9ydLO6zpbV4RYDgtsaGQxOxVc0y1Lj8CWTi/XYIVRnmqrjGmubRV7q8pTxrg\n" + + "oyk2zwQ+twyxp/8ZRHzl5ISiDLKSDlcMK1oa7NqyL+MCwiswpaObk56HxgF2ZwEb\n" + + "JZYCwetxyTK7HX4/WV0V6TaPzS7dHAsb6t1Aq8IS1JdGjWKRPkjkhR95nQARAQAB\n" + + "zSNQYXVsIFNjaGF1YiA8dmFuaXRhc3ZpdGFlQGZzZmUub3JnPsLEAgQTAQoCrAIb\n" + + "AwYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4ACGQEWIQR/kRb+qQpZg5NsfPqgJ9sv\n" + + "Ph4RigUCYAwbLDUUgAAAAAASABpwcm9vZkBtZXRhY29kZS5iaXpkbnM6amFiYmVy\n" + + "aGVhZC50az90eXBlPVRYVD4UgAAAAAASACNwcm9vZkBtZXRhY29kZS5iaXpodHRw\n" + + "czovL2Zvc3N0b2Rvbi5vcmcvQHZhbml0YXN2aXRhZZAUgAAAAAASAHVwcm9vZkBt\n" + + "ZXRhY29kZS5iaXp4bXBwOnZhbml0YXN2aXRhZUBqYWJiZXJoZWFkLnRrP29tZW1v\n" + + "LXNpZC0yMDkzNjgxNTQ1PTYyODlhYTNiZDhhNTAxYTM2MzIyYTBmODk0ZjhkMWQ5\n" + + "NzE4ZGVkMDM2MTYwMzlmMWNmNDhiMmE0MWVlMzU5MjCPFIAAAAAAEgB0cHJvb2ZA\n" + + "bWV0YWNvZGUuYml6eG1wcDp2YW5pdGFzdml0YWVAamFiYmVyaGVhZC50az9vbWVt\n" + + "by1zaWQtMTk5MTQxODIwPWY0YThmZjg0MDA0MzkzYTg3ZjcwMTNjNjAwNjViZGM4\n" + + "OWIxMTY5ZWJjZmI4MDYwYzRmOTY2OWI0M2JhMGM4MTSQFIAAAAAAEgB1cHJvb2ZA\n" + + "bWV0YWNvZGUuYml6eG1wcDp2YW5pdGFzdml0YWVAamFiYmVyaGVhZC50az9vbWVt\n" + + "by1zaWQtMTQyOTY3NzEyNT1lOGE3YjEyMzZiODUwYjQ2N2E1MDkyYzBiZGZlYTg2\n" + + "YTUzZTM2ODQyODNhMWQ1ZmUyNmVmNTg3MmRkMGFlZjQxSBSAAAAAABIALXByb29m\n" + + "QG1ldGFjb2RlLmJpemh0dHBzOi8vY29kZWJlcmcub3JnL3Zhbml0YXN2aXRhZS9n\n" + + "aXRlYV9wcm9vZgAKCRCgJ9svPh4RivdTEADC3xMcrcDR/+4JlDl5fblecfJHr3/E\n" + + "0fzkPWJJBL+TIn3ON2sSKIfLn9M7NYWIGT0QLI4LnqT+SZ3Ont1h8irM4O8LuTwZ\n" + + "kqjLkytGhgCErSdGzJ3oIcdXcnzX/p6fmxer1Qg/bpFy8mRrpSQ5tI0TYUXfD0qs\n" + + "BEbUhB3Tsg8AYaDRcdPx8gf1METZDxx/E6RQNzVIfyCK8hszzU1pRFr15DYDCjl5\n" + + "RZjTxXqxJFKUz85LvQToaFo5SXgH/fWf0EeoD+YNqyhROYr8iWMLCLiHqvqkEXny\n" + + "lm7qNlFxFGFSu8Mcj6HSet5qvRj2wn6XssOWm2pOalDJx+L/biETr5vEnBwfw7p2\n" + + "1Pmrg/jhK9yasKsdYKRlJdJWOtpEi9amcQ4sGA9OD74weJ/zEEPgLKbvkWFuUy8a\n" + + "69AEeKAbB3RH3r7+PRnPVvxC3MpEmLsRsjVdP21xGhtnqAzJFkMRXf5lpC6czJiH\n" + + "gd/sao0mJPrkWUHDn0k9rgoZI9gRRENk3tXefjwQ2A5aEcAagmb2l0DjugYAb7dU\n" + + "ip9bJNUhBgjiaWYBj9uZOzYdQ7kFcFWp7iCGvkoeBMQf29rXZOZsxQmKLgEPZuCl\n" + + "YmIO4PS6sERoPT+FUGl85YAkEIBII0TCQdVQd/Vx6JRLc/f/cFCoKBv2+9LKVPIp\n" + + "wNNL5J+0m/H1dMLDzAQTAQoCdgIbAwYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AC\n" + + "GQEWIQR/kRb+qQpZg5NsfPqgJ9svPh4RigUCX8gjtUgUgAAAAAASAC1wcm9vZkBt\n" + + "ZXRhY29kZS5iaXpodHRwczovL2NvZGViZXJnLm9yZy92YW5pdGFzdml0YWUvZ2l0\n" + + "ZWFfcHJvb2aQFIAAAAAAEgB1cHJvb2ZAbWV0YWNvZGUuYml6eG1wcDp2YW5pdGFz\n" + + "dml0YWVAamFiYmVyaGVhZC50az9vbWVtby1zaWQtMTQyOTY3NzEyNT1lOGE3YjEy\n" + + "MzZiODUwYjQ2N2E1MDkyYzBiZGZlYTg2YTUzZTM2ODQyODNhMWQ1ZmUyNmVmNTg3\n" + + "MmRkMGFlZjQxjxSAAAAAABIAdHByb29mQG1ldGFjb2RlLmJpenhtcHA6dmFuaXRh\n" + + "c3ZpdGFlQGphYmJlcmhlYWQudGs/b21lbW8tc2lkLTE5OTE0MTgyMD1mNGE4ZmY4\n" + + "NDAwNDM5M2E4N2Y3MDEzYzYwMDY1YmRjODliMTE2OWViY2ZiODA2MGM0Zjk2Njli\n" + + "NDNiYTBjODE0kBSAAAAAABIAdXByb29mQG1ldGFjb2RlLmJpenhtcHA6dmFuaXRh\n" + + "c3ZpdGFlQGphYmJlcmhlYWQudGs/b21lbW8tc2lkLTIwOTM2ODE1NDU9NjI4OWFh\n" + + "M2JkOGE1MDFhMzYzMjJhMGY4OTRmOGQxZDk3MThkZWQwMzYxNjAzOWYxY2Y0OGIy\n" + + "YTQxZWUzNTkyMD4UgAAAAAASACNwcm9vZkBtZXRhY29kZS5iaXpodHRwczovL2Zv\n" + + "c3N0b2Rvbi5vcmcvQHZhbml0YXN2aXRhZQAKCRCgJ9svPh4RiiRwD/47o9xzTDXB\n" + + "thNwd/T1UWKSNtLoPX6V4V2hUW/z1SZulba9i041fM04yaauqOFrKfoFJjovdZis\n" + + "UZeYs0Bfjf87JoJwN6TgX/7bQjSncBKHmKDXI7SLuY9dtYvqGCUOlVPTr4lxm1Ht\n" + + "CK5XJWzMjE/mUaPwUeP8agG2lRko46K2O4msUGvnZt/m6ggtyn7WhdxHAMEiBxmk\n" + + "j0lTIj5Q78hMxlWCI7D9bSNkRSHKN+5AQ0OIQCQnvbh1Gz85DO+VJdtr529L5pz+\n" + + "WEsrApGbjhi3UYfIS5fBTMfIcOZ8gs7fty79LOBuweAKKWnLt6jrRlBZ16D8LuM+\n" + + "1nrPUzTIanuqFLiysBhKBrX16UCKsW+kRvWLRG4AnEdWVlJr79kSzbzVYPHwKBqb\n" + + "41fagZdQdxt0xZcA2wGdV7UKLbY+rNew4PC9Lt+nS6pnItT0hlSVdPOBKoieoLR0\n" + + "XQAPM+Cr1qGlCFWNbMq6Q5ssS3kbTULd7UTKZuD9Wp+7h8zHqB8GoffaIT0Vvl0x\n" + + "t2TPM9+GJIkS3K+JQOGpPMrT2qRt9sL8J8u2usk/KOiD2uqu0QH3I+0qkvakFc24\n" + + "sGnj1XmIg46vYEF1N+E8kjzkIKkxoX/1sTKd5EHnw2ivOxLQM3B2PGNAn2N4S9eF\n" + + "qN+60sNMNXmlptdlVuOxdeJBSeF0vXFZ2cLDgwQTAQoCLQIbAwYLCQgHAwIGFQgC\n" + + "CQoLBBYCAwECHgECF4ACGQEWIQR/kRb+qQpZg5NsfPqgJ9svPh4RigUCX8giLj4U\n" + + "gAAAAAASACNwcm9vZkBtZXRhY29kZS5iaXpodHRwczovL2Zvc3N0b2Rvbi5vcmcv\n" + + "QHZhbml0YXN2aXRhZZAUgAAAAAASAHVwcm9vZkBtZXRhY29kZS5iaXp4bXBwOnZh\n" + + "bml0YXN2aXRhZUBqYWJiZXJoZWFkLnRrP29tZW1vLXNpZC0yMDkzNjgxNTQ1PTYy\n" + + "ODlhYTNiZDhhNTAxYTM2MzIyYTBmODk0ZjhkMWQ5NzE4ZGVkMDM2MTYwMzlmMWNm\n" + + "NDhiMmE0MWVlMzU5MjCPFIAAAAAAEgB0cHJvb2ZAbWV0YWNvZGUuYml6eG1wcDp2\n" + + "YW5pdGFzdml0YWVAamFiYmVyaGVhZC50az9vbWVtby1zaWQtMTk5MTQxODIwPWY0\n" + + "YThmZjg0MDA0MzkzYTg3ZjcwMTNjNjAwNjViZGM4OWIxMTY5ZWJjZmI4MDYwYzRm\n" + + "OTY2OWI0M2JhMGM4MTSQFIAAAAAAEgB1cHJvb2ZAbWV0YWNvZGUuYml6eG1wcDp2\n" + + "YW5pdGFzdml0YWVAamFiYmVyaGVhZC50az9vbWVtby1zaWQtMTQyOTY3NzEyNT1l\n" + + "OGE3YjEyMzZiODUwYjQ2N2E1MDkyYzBiZGZlYTg2YTUzZTM2ODQyODNhMWQ1ZmUy\n" + + "NmVmNTg3MmRkMGFlZjQxAAoJEKAn2y8+HhGKcCkP+gPiUroUSbVfJzFyWej0EPF1\n" + + "773h5aVoKgZ4gtVYSupM4rudP0oP/tH8sjSFebetpgyKEfZqau3lGbiWaIjXgNRW\n" + + "+9Tyi201tJbg/sAMczhK9ikGM0RtzI0oA1YK5DFYA8ImCfxkv7ZDi3/AiUzPei/6\n" + + "ja4g417ueNw8kp12Jh3jErWWHpeideHpcKg9vbbXO9GJ/nNWKXLwBAGhTKNAulby\n" + + "CYMfXqG1xKiWchDI9BylNF5bSPz5Yoxz91QBAR7X5x77rhSmg0zWkMIbla8VMrzX\n" + + "ZvfypFMeQeju3qRzLmAsSUr8JCg0q7q9tePQynn/wvcRoPGPxLLEsHdcOM2j5e3G\n" + + "+jU+gDsOVCpyEYP70OGsF8duR/iNCJ+pso1JPu2I+5NSGeIYfejuoa0AoHUt6yHs\n" + + "+K2bGh3hEFz8jyxp27GvcQvwAYDDaZ+RQRdAo4DKXb9Y/mqxvrm8GsbB+puzrIxw\n" + + "be3/iAw47ANJG0RbuDVlycBEwGImAKhQ24fM1/QFhs3YyRPg2jqOujOrcgYVC599\n" + + "XSGMwcdpS/dka0l77rkMK2WKk1R0+cfwM/XItMti/dVgfMPstfjO3xc8E5LAxZIv\n" + + "n9yfLIdS87jqgw1mUKF9PSFC53v7cQppYlt6tztFjo8HWisiP7LRkSR+wR+HKjSL\n" + + "Ek3f6fF97SSUREcxN2cfwsNEBBMBCgHuAhsDBgsJCAcDAgYVCAIJCgsEFgIDAQIe\n" + + "AQIXgJAUgAAAAAASAHVwcm9vZkBtZXRhY29kZS5iaXp4bXBwOnZhbml0YXN2aXRh\n" + + "ZUBqYWJiZXJoZWFkLnRrP29tZW1vLXNpZC0xNDI5Njc3MTI1PWU4YTdiMTIzNmI4\n" + + "NTBiNDY3YTUwOTJjMGJkZmVhODZhNTNlMzY4NDI4M2ExZDVmZTI2ZWY1ODcyZGQw\n" + + "YWVmNDGPFIAAAAAAEgB0cHJvb2ZAbWV0YWNvZGUuYml6eG1wcDp2YW5pdGFzdml0\n" + + "YWVAamFiYmVyaGVhZC50az9vbWVtby1zaWQtMTk5MTQxODIwPWY0YThmZjg0MDA0\n" + + "MzkzYTg3ZjcwMTNjNjAwNjViZGM4OWIxMTY5ZWJjZmI4MDYwYzRmOTY2OWI0M2Jh\n" + + "MGM4MTSQFIAAAAAAEgB1cHJvb2ZAbWV0YWNvZGUuYml6eG1wcDp2YW5pdGFzdml0\n" + + "YWVAamFiYmVyaGVhZC50az9vbWVtby1zaWQtMjA5MzY4MTU0NT02Mjg5YWEzYmQ4\n" + + "YTUwMWEzNjMyMmEwZjg5NGY4ZDFkOTcxOGRlZDAzNjE2MDM5ZjFjZjQ4YjJhNDFl\n" + + "ZTM1OTIwFiEEf5EW/qkKWYOTbHz6oCfbLz4eEYoFAl/IHp4CGQEACgkQoCfbLz4e\n" + + "EYqTOA/+OubamE0ivV15sXOLbVTYoPYgy21lJilGXnV7JBcSixRDEupTIaWqZwB4\n" + + "YVtA8hbyXOMgA96VT0SJ93rN7WDQYCiPjF+oQD2yo24rHxj831SNjPQBjjQiCVtA\n" + + "aYOvqfgE9peUgAmGxB0JZ9CDCjQFxzV0lAhsb1KlWNNCqTNYqWWlwRdziKeKoUEH\n" + + "//fiQvWRK7NZbbnNj6rKKo4CnfXKuVCzKDNIeq3vf877k+EIwyNXVlgghFaqTjP8\n" + + "kUVD0clmtS6fBwZ+LbQydo3yEQ66/mbkjYJ1lpO3hn2hvHXn/kZE7qRmWe/frIMU\n" + + "Z6niuKaAoPErYQyMTuQ/dFRbsqT6cXHw1mGkuoqiLp6wccb5JrfaszVbUF3MIdZF\n" + + "041uQqYJvaATgCsM236cgRCpfxlc/8YC2C5PK0oMyYTiHe910PB0aYY1v2IEOnpq\n" + + "LP+0hdOET0bzTBVwsq9fD4YxNclw4mYHZ439TezI+Fnr47OuIS/BrWWOxBrFdTnL\n" + + "eHBL42/5+i46jbdE6RKU+Kpb0byWr/jYkm9AZVp1/zHBU31u/TpEFXE/Imn0bauH\n" + + "ubiBC9L+8Oy4SMrCLdcclfG4Sk3JaBDgetAZLslzxSXEMl9C2tHFSgyO8Xx+5KNK\n" + + "TZx5n04SWFFUgNZIYATCV70QpVAgagkSrNwrpV2QcfcsFbACiDzCw0EEEwEKAesC\n" + + "GwMGCwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAFiEEf5EW/qkKWYOTbHz6oCfbLz4e\n" + + "EYoFAl/IG5qQFIAAAAAAEgB1cHJvb2ZAbWV0YWNvZGUuYml6eG1wcDp2YW5pdGFz\n" + + "dml0YWVAamFiYmVyaGVhZC50az9vbWVtby1zaWQtMTQyOTY3NzEyNT1lOGE3YjEy\n" + + "MzZiODUwYjQ2N2E1MDkyYzBiZGZlYTg2YTUzZTM2ODQyODNhMWQ1ZmUyNmVmNTg3\n" + + "MmRkMGFlZjQxjxSAAAAAABIAdHByb29mQG1ldGFjb2RlLmJpenhtcHA6dmFuaXRh\n" + + "c3ZpdGFlQGphYmJlcmhlYWQudGs/b21lbW8tc2lkLTE5OTE0MTgyMD1mNGE4ZmY4\n" + + "NDAwNDM5M2E4N2Y3MDEzYzYwMDY1YmRjODliMTE2OWViY2ZiODA2MGM0Zjk2Njli\n" + + "NDNiYTBjODE0kBSAAAAAABIAdXByb29mQG1ldGFjb2RlLmJpenhtcHA6dmFuaXRh\n" + + "c3ZpdGFlQGphYmJlcmhlYWQudGs/b21lbW8tc2lkLTIwOTM2ODE1NDU9NjI4OWFh\n" + + "M2JkOGE1MDFhMzYzMjJhMGY4OTRmOGQxZDk3MThkZWQwMzYxNjAzOWYxY2Y0OGIy\n" + + "YTQxZWUzNTkyMAAKCRCgJ9svPh4RioY0EAC2URLKbRQTP97apJ7qctk9dWOKgx+m\n" + + "xLqCmo0d4uH7phxx6VAXLXCJRwPhrvOekUL4xRC2qYyO0Zit4yXM7HbxqC8lScMX\n" + + "z98siF7aAXWjJ+2UpIaoP75jpUPs0t0Ude1gQ6UqPqLJI/yQLWVtAaa8IqEFBvFR\n" + + "sOg4T2MuZdUo75r/PApL1npZcHhNUSwagHYOY6lCKAlpMxitEhxPR07Ji5llIKGV\n" + + "d1wyOxzP09IXsfeKME2zfrTSf9ybHEVAUQdjybXcEaB+carkw/gzKrxEgrACnEOe\n" + + "CvWwd0Fq84F9U+MTnikIhdcSTUDAVRbFQQnxdd+G6QocYc15jtdOFnnZvXI9FuWf\n" + + "asEKlCdqT/bv6a/Nvba0AE9aAIkbCwr3I4Dx93alVPPMBiFpvG6p7mMQLQAL4IsA\n" + + "m+OinigQy2BtsSd/fwP851QFJNc8aUf9dvu0zq8f/rFNk+V58SEKVXEOtSYK7tcI\n" + + "A+mrkL/jwkwFas2Uh3ZirkqVq4KFtJA2jlW3m14TTyuk6IGxP5SB5RjTY2nlJbJw\n" + + "+jO1AkAONomoy0uzuAxlJAFxUjhEpHNU5MuNCYPIlmplkSoCdEv1pc7c1ZnB2zjP\n" + + "FUqsx69gorhoIEGeMfi1XGxuHy2I3GbmFtA681Vgzl5FppV2v7X9jicL4GruAkwH\n" + + "zUz1S6VeXnelcMLBjwQTAQIAIgUCV/PW5wIbAwYLCQgHAwIGFQgCCQoLBBYCAwEC\n" + + "HgECF4AAIQkQoCfbLz4eEYoWIQR/kRb+qQpZg5NsfPqgJ9svPh4RikX2EACFH0OF\n" + + "okyqKs4hJTeIW5i3nMYID1F3vfusmDFfpcltue+2LdEvrj1rhOXfvOpNSWLWUzJa\n" + + "O46tH813WSBncMwSlo+6zAkojcOnf0fC08RlDSimioXG4dOcs9pd3TPKxEMOTQYs\n" + + "kGbyRUrvg6Hl+zv7eXRyyMFMQYAwOQJ9pIf5AGp5ObJ2RU87IOxKH/jTjAV6yDvr\n" + + "RrBii8NhVr4ouj7c/UflLKLgZ/8RJxcUL5yFInTfbaEMBnQv20AMsAqFR+1VTQ5M\n" + + "flLfa7eK+g2lPpCXaZaNrzZkdWk6GggAg4A/6Ighx/VxaPY8PI5K0j7C/PUiKSxQ\n" + + "pHHIwuEOZG4Uy33iOjT6n9oiHSMF3iNbf4zvs1Gv5IJOgv1xgU+ppfLF3o322NTh\n" + + "t5YXLnbMXPGSh6SvxLlBUxI8gjQdjfaJol0oz31UDedF+CElD7SJbJIPKJq4NBqe\n" + + "kQjNUFuHNRouXWNjpX5jlTGx8VM4jUzKISo5I1UvGbUZRxteyWWyFJgbr7VCH2+e\n" + + "aENvN215GHWi63EE8Qkp/euTBqA2U69E6vHxwhw+5NA9zE4J0C9yn1JsqBqjPgpt\n" + + "emn14QJeJw+yms+BXzAASZY4CL/OGHS40BJgpV7n9GNF8OrZuEZZM+dfzgVd9r4S\n" + + "Ogq+ogmrA7DvTpM4OA9Cu+wVVXQRL/BNndEdjc0mUGF1bCBTY2hhdWIgPHZhbml0\n" + + "YXN2aXRhZUBtYWlsYm94Lm9yZz7Cw/4EEwEKAqgCGwMFCwkIBwMFFQoJCAsFFgID\n" + + "AQACHgECF4AWIQR/kRb+qQpZg5NsfPqgJ9svPh4RigUCYAwbOTUUgAAAAAASABpw\n" + + "cm9vZkBtZXRhY29kZS5iaXpkbnM6amFiYmVyaGVhZC50az90eXBlPVRYVD4UgAAA\n" + + "AAASACNwcm9vZkBtZXRhY29kZS5iaXpodHRwczovL2Zvc3N0b2Rvbi5vcmcvQHZh\n" + + "bml0YXN2aXRhZZAUgAAAAAASAHVwcm9vZkBtZXRhY29kZS5iaXp4bXBwOnZhbml0\n" + + "YXN2aXRhZUBqYWJiZXJoZWFkLnRrP29tZW1vLXNpZC0yMDkzNjgxNTQ1PTYyODlh\n" + + "YTNiZDhhNTAxYTM2MzIyYTBmODk0ZjhkMWQ5NzE4ZGVkMDM2MTYwMzlmMWNmNDhi\n" + + "MmE0MWVlMzU5MjCPFIAAAAAAEgB0cHJvb2ZAbWV0YWNvZGUuYml6eG1wcDp2YW5p\n" + + "dGFzdml0YWVAamFiYmVyaGVhZC50az9vbWVtby1zaWQtMTk5MTQxODIwPWY0YThm\n" + + "Zjg0MDA0MzkzYTg3ZjcwMTNjNjAwNjViZGM4OWIxMTY5ZWJjZmI4MDYwYzRmOTY2\n" + + "OWI0M2JhMGM4MTSQFIAAAAAAEgB1cHJvb2ZAbWV0YWNvZGUuYml6eG1wcDp2YW5p\n" + + "dGFzdml0YWVAamFiYmVyaGVhZC50az9vbWVtby1zaWQtMTQyOTY3NzEyNT1lOGE3\n" + + "YjEyMzZiODUwYjQ2N2E1MDkyYzBiZGZlYTg2YTUzZTM2ODQyODNhMWQ1ZmUyNmVm\n" + + "NTg3MmRkMGFlZjQxSBSAAAAAABIALXByb29mQG1ldGFjb2RlLmJpemh0dHBzOi8v\n" + + "Y29kZWJlcmcub3JnL3Zhbml0YXN2aXRhZS9naXRlYV9wcm9vZgAKCRCgJ9svPh4R\n" + + "ivVhD/46gD755fsVTqanw0VUq9HCWEmSGu5jIU6USs8ZD71Jb1uivXjjKVM4Ir8a\n" + + "BZW7+HNrz+XoRfztExxnwh90GVTWYkdrM44x3dOBxQ33etW41yqkmdHHbDnJ45Oj\n" + + "23RBp7zSEHmG5TZyvSU5aWUVw+QEqV6uzt43XYL5z3Nnt9RKs9CEAXcrKxOi9FLs\n" + + "V/g9xARlfsNw5J4LxoTYV856qPabb4VZy/6TRKxWMJXFQg55xODKgMm+Us2C97db\n" + + "6d4rrGH+XFE5rwKNbJH8m3bsHxEwdleIWX270cwtd769FeAydtjte9kTNNJ+9JGG\n" + + "Pj2LbhRkf8gnnvQxzyOdiMQ59cAz4rrgVviB0wXOEqhgjxxmIg3e3Y3pncnXRzZm\n" + + "v2ShxzpUw7UWK25S3TDBVcHRE0IpOm0eOMQq5kWGy+pEUm1IbJz+kPb0cI9x+VhZ\n" + + "k4nnni4yrhAooBcxn5gkKlQc3FFiM8gqw6duj68ugheL/CtJYuYFdJoKtSajzKSD\n" + + "vn/64t+rvPY1eywmOgaQ7ljZXEYO3KrgILaKZp5quTY4HY644OMSFboOphLQ2yMm\n" + + "ZNUMeYKyHNu5Nw6qyrhcpCLEQ8D5RK63YLuvyDIn+psseOCjjNQhjSRTyYfV4cfW\n" + + "C7Bgs9j14xh7t77CY7OtOjWof2mHSzAerMIr5F698BeqMx9DHsLDyAQTAQoCcgIb\n" + + "AwULCQgHAwUVCgkICwUWAgMBAAIeAQIXgBYhBH+RFv6pClmDk2x8+qAn2y8+HhGK\n" + + "BQJfyCO2SBSAAAAAABIALXByb29mQG1ldGFjb2RlLmJpemh0dHBzOi8vY29kZWJl\n" + + "cmcub3JnL3Zhbml0YXN2aXRhZS9naXRlYV9wcm9vZpAUgAAAAAASAHVwcm9vZkBt\n" + + "ZXRhY29kZS5iaXp4bXBwOnZhbml0YXN2aXRhZUBqYWJiZXJoZWFkLnRrP29tZW1v\n" + + "LXNpZC0xNDI5Njc3MTI1PWU4YTdiMTIzNmI4NTBiNDY3YTUwOTJjMGJkZmVhODZh\n" + + "NTNlMzY4NDI4M2ExZDVmZTI2ZWY1ODcyZGQwYWVmNDGPFIAAAAAAEgB0cHJvb2ZA\n" + + "bWV0YWNvZGUuYml6eG1wcDp2YW5pdGFzdml0YWVAamFiYmVyaGVhZC50az9vbWVt\n" + + "by1zaWQtMTk5MTQxODIwPWY0YThmZjg0MDA0MzkzYTg3ZjcwMTNjNjAwNjViZGM4\n" + + "OWIxMTY5ZWJjZmI4MDYwYzRmOTY2OWI0M2JhMGM4MTSQFIAAAAAAEgB1cHJvb2ZA\n" + + "bWV0YWNvZGUuYml6eG1wcDp2YW5pdGFzdml0YWVAamFiYmVyaGVhZC50az9vbWVt\n" + + "by1zaWQtMjA5MzY4MTU0NT02Mjg5YWEzYmQ4YTUwMWEzNjMyMmEwZjg5NGY4ZDFk\n" + + "OTcxOGRlZDAzNjE2MDM5ZjFjZjQ4YjJhNDFlZTM1OTIwPhSAAAAAABIAI3Byb29m\n" + + "QG1ldGFjb2RlLmJpemh0dHBzOi8vZm9zc3RvZG9uLm9yZy9AdmFuaXRhc3ZpdGFl\n" + + "AAoJEKAn2y8+HhGK7R4P/RtmQN/Q39Jj+v4pPWxetHRAqFasLoZnFCj1rYgHE7z1\n" + + "hWqhCFMaCgeM3r63knwNNQhbZ2KTGhw1tjC/yfWnvDrhQkm1Idr6Zpn9v/D3KIXM\n" + + "s4bdPMlRUpRXOE/AM+RS08/bouE7CqIwv0oAj3VOMiMazRYLwXAfkJtUzgWNqlwX\n" + + "pujDtAJB6M11XM/Q6qeM4j1pjvXJs/faUFHXyku1zH4rcR0go79qyAbZ1vS67Ps/\n" + + "Wg5QYpklc80XarpHRtFVFWagGEtM0mkazkyYBgySZRm8miDGEuwm2HzDru0x+Clp\n" + + "H7uSDy6uiOjJO6+ApbJxkWDH/POuwpd0fCLwI9C4UAEnZLkCE3iXNbTKguXEc1Rb\n" + + "t8nxrMVlhQO35+1AVo9rpr/8r+FZRlYfZYEB4sUtxjbbIpFV0YZkOBiAW3r6Tp0X\n" + + "YJU8wi/fChJvF4j81grwckQavRDbsuQEEbnYzwjucpw2D+Ug/6U+Dhjj1qeYuxJG\n" + + "VfF+S07d3k2h84IzElcPwoP5uxpe2MdIOQY+EK0D3mpfedmrlkv8wnImMKp9dU2N\n" + + "tG4YqAikdAy4akbU03nk6GVFrGo3gLDKXBA9GVNbnjW9qd0S3OI64Ci+8mBg3NBN\n" + + "yIX5uLPsN1+PrwwzQuOBw/gSeWs9JhJA4C8emlzwb+sT8mw+h3ZZ8EI81LD+0h09\n" + + "wsN/BBMBCgIpAhsDBQsJCAcDBRUKCQgLBRYCAwEAAh4BAheAFiEEf5EW/qkKWYOT\n" + + "bHz6oCfbLz4eEYoFAl/IIjU+FIAAAAAAEgAjcHJvb2ZAbWV0YWNvZGUuYml6aHR0\n" + + "cHM6Ly9mb3NzdG9kb24ub3JnL0B2YW5pdGFzdml0YWWQFIAAAAAAEgB1cHJvb2ZA\n" + + "bWV0YWNvZGUuYml6eG1wcDp2YW5pdGFzdml0YWVAamFiYmVyaGVhZC50az9vbWVt\n" + + "by1zaWQtMjA5MzY4MTU0NT02Mjg5YWEzYmQ4YTUwMWEzNjMyMmEwZjg5NGY4ZDFk\n" + + "OTcxOGRlZDAzNjE2MDM5ZjFjZjQ4YjJhNDFlZTM1OTIwjxSAAAAAABIAdHByb29m\n" + + "QG1ldGFjb2RlLmJpenhtcHA6dmFuaXRhc3ZpdGFlQGphYmJlcmhlYWQudGs/b21l\n" + + "bW8tc2lkLTE5OTE0MTgyMD1mNGE4ZmY4NDAwNDM5M2E4N2Y3MDEzYzYwMDY1YmRj\n" + + "ODliMTE2OWViY2ZiODA2MGM0Zjk2NjliNDNiYTBjODE0kBSAAAAAABIAdXByb29m\n" + + "QG1ldGFjb2RlLmJpenhtcHA6dmFuaXRhc3ZpdGFlQGphYmJlcmhlYWQudGs/b21l\n" + + "bW8tc2lkLTE0Mjk2NzcxMjU9ZThhN2IxMjM2Yjg1MGI0NjdhNTA5MmMwYmRmZWE4\n" + + "NmE1M2UzNjg0MjgzYTFkNWZlMjZlZjU4NzJkZDBhZWY0MQAKCRCgJ9svPh4Riuaw\n" + + "D/9zil7na4utYS7e87CDlnUZT1JmWFRB/fglMG6B3dV1I+wIqsCIYWEkkobJlBI4\n" + + "YLYqx3UrYn/TGEca6y6pzlhbRk7YaY+z31XSWZj+fuRBZLLx2WTRgH1L3brQn5+k\n" + + "AHkUx2cS1R1usTxqFqWp+APbdDGDpzvHp8omtaYqecAaOhJp3AN96kdsyXCR/SeY\n" + + "Kc8aghCBqQx1uhXjyATO3OE+nD/DtWU7z/wqR2LrIvIzrUIQW76FgaqPMSf922p8\n" + + "1GxFvAHIa81SGptYDPq7kNXqG1LVF/NJBJAqxCZhu/yIrx+jus7+g3XaoEbuGtO/\n" + + "SPxpdDcKiuRwRer1MznX0cbzE2DoaI21t9kJ3y9l8QBl7xLHSCXYxF+hxBy3w7Nq\n" + + "TeGpdclC2uMV05H43vKk4Ecrax96g8Bwt6J+jpDPw0LbOBbwGKs5P5ggugtlSFFG\n" + + "jMmuIfd+s89lhXzTkBirkM8rEcLrORXww1meaxlhZ8gqHP/amWvNIG/Rpoa+oMs2\n" + + "ArA4BJpSeK58pPKH+kL+uZzbfIHZORM54hnuyDOYiMAjdjETETK4QJuNdHkniEU2\n" + + "FkHZVYmmdP2Vtjx9XWoFoWjAg2V4XPo87p4GUzwLQ12YS8tNkZMdtUOf0sOKE45E\n" + + "EhAb4jxjIcWFQdX99YrtG9Pb8H8KlJeMunQwyLGUcbmt7cLDQAQTAQoB6gIbAwUL\n" + + "CQgHAwUVCgkICwUWAgMBAAIeAQIXgBYhBH+RFv6pClmDk2x8+qAn2y8+HhGKBQJf\n" + + "yBuakBSAAAAAABIAdXByb29mQG1ldGFjb2RlLmJpenhtcHA6dmFuaXRhc3ZpdGFl\n" + + "QGphYmJlcmhlYWQudGs/b21lbW8tc2lkLTE0Mjk2NzcxMjU9ZThhN2IxMjM2Yjg1\n" + + "MGI0NjdhNTA5MmMwYmRmZWE4NmE1M2UzNjg0MjgzYTFkNWZlMjZlZjU4NzJkZDBh\n" + + "ZWY0MY8UgAAAAAASAHRwcm9vZkBtZXRhY29kZS5iaXp4bXBwOnZhbml0YXN2aXRh\n" + + "ZUBqYWJiZXJoZWFkLnRrP29tZW1vLXNpZC0xOTkxNDE4MjA9ZjRhOGZmODQwMDQz\n" + + "OTNhODdmNzAxM2M2MDA2NWJkYzg5YjExNjllYmNmYjgwNjBjNGY5NjY5YjQzYmEw\n" + + "YzgxNJAUgAAAAAASAHVwcm9vZkBtZXRhY29kZS5iaXp4bXBwOnZhbml0YXN2aXRh\n" + + "ZUBqYWJiZXJoZWFkLnRrP29tZW1vLXNpZC0yMDkzNjgxNTQ1PTYyODlhYTNiZDhh\n" + + "NTAxYTM2MzIyYTBmODk0ZjhkMWQ5NzE4ZGVkMDM2MTYwMzlmMWNmNDhiMmE0MWVl\n" + + "MzU5MjAACgkQoCfbLz4eEYq+WhAAxE+FWFauoqKvk7m9XfV9m1v8o9jzialXMo92\n" + + "pbyH0TZl2L8H8zUxxJIgvwdgHxvlqLnK95mDNKkRi2qCLhtLVAy04W4n0h7D+//D\n" + + "5pCbvMokU4LKYWNL8Rtv0cBIFxpUI2xdVdAG5E3pLimdcpE5/IpHAj+ImkF+8rNk\n" + + "yKUHwTUZ24PKgugdzI5zp0UUZ3QrLe4PxOrZif3UURhzej2751+5GSZixZQN+eWl\n" + + "L+CldUaWTG4I6e93FpepX3gCpPJo5zMbTlDZG9dQFFMY/jfxNf84MlfDOp5EuIYO\n" + + "v4QrG1EdYn9xMBdDilK5lWzAh2flQx3Oi2y5jFIGYX8enUJeMrsbtchbcWhS6O/u\n" + + "fefSAAAriw3r4CLQrJ1eyH5DHK2nh6leNP8hXmiV7c1TzK/KMI8uiDQ13Wp2Utoq\n" + + "hLVs1tXfM1EMzGoPXQIMDdbOqtJCtjFVlRsBDu/pp1+IppTpq9+ftqHXoB3+nMrh\n" + + "mV7r2/BMyR+q88PfJGahxQc0w82YZjaMufWfaDIixDpVtRFNSzbWmz7AA+ylOSOv\n" + + "lJKHpJVHo7YP7h23jhqOc25vZ+JQS1YQ00IYFMg86T/7Xq0gttSYLf2deZHnKF8E\n" + + "mEoZL0UY2tqOZfXl+Ge+w4QsV01WrXmzcBLydGneACdJ6Luk40kwWO70VEkK+Ed5\n" + + "u64eyejCwY4EEwEKADgWIQR/kRb+qQpZg5NsfPqgJ9svPh4RigUCXJkXLAIbAwUL\n" + + "CQgHAwUVCgkICwUWAgMBAAIeAQIXgAAKCRCgJ9svPh4RipPtD/0duXEGR8m82Pbj\n" + + "zivuW0HCyLIxsbhvWYyBlbENo2qvX+zWl2n4Q24n6nfTOh+6WNLc9MHworhO3laC\n" + + "9syN4CLgv14cbSCAdTsLaDpOpLBTkwFhEI2gFEiKGaNRnRrf6oGci9q5O4DTkYtk\n" + + "ARZHq9e0tWA/rYYcsQQrRbj+eG50Lirwn39CwvPlMx5Gag50jThyUb2qbyOXJAkb\n" + + "7R6UxRvHOKJxjZqW0qp8F5GPBjqRhqcVQ6BypAHsvnhiOtZPiagQSovf6U1gHMU5\n" + + "kysuybtPMoxesa/U2ZtOs6xvDv2JF+Lscbg/wB1nIe1VwIuzrN80fXB1IGn+Dxl8\n" + + "hYTFUn7iJuVhPgAkmN4m6+hD6EQcOB+SLO+rJKFNTaVAL4w79onDgVQGJR/FspBI\n" + + "aHTPUaC3zV8G+91SUFPV37e64+FgPFEGu15UcXJdt3/m1dO/nDu/YU8xC0TMyPk/\n" + + "llIc+vNl/IxhT0Y8FEHL+WJWZQ9FyBxXBILlP5THuUwnedCuhnlO46GDmZSxHxh7\n" + + "CoMF19QxMQ6Qf0uDnnr0vMfnYQuwdEcushJHam1XwWe7kvPao0irq1r8tab2BFhP\n" + + "AnnY+nl4e9S23IkkVbbmCXaRM5QCmwgrLY/XggfcVxqb82qBp8irYHRiIjVAEElB\n" + + "HJrJWeCQnhVM18nrAbG3ic6sAeB/8s0lUGF1bCBTY2hhdWIgPHZhbml0YXN2aXRh\n" + + "ZUByaXNldXAubmV0PsLD/gQTAQoCqAIbAwULCQgHAwUVCgkICwUWAgMBAAIeAQIX\n" + + "gBYhBH+RFv6pClmDk2x8+qAn2y8+HhGKBQJgDBs5NRSAAAAAABIAGnByb29mQG1l\n" + + "dGFjb2RlLmJpemRuczpqYWJiZXJoZWFkLnRrP3R5cGU9VFhUPhSAAAAAABIAI3By\n" + + "b29mQG1ldGFjb2RlLmJpemh0dHBzOi8vZm9zc3RvZG9uLm9yZy9AdmFuaXRhc3Zp\n" + + "dGFlkBSAAAAAABIAdXByb29mQG1ldGFjb2RlLmJpenhtcHA6dmFuaXRhc3ZpdGFl\n" + + "QGphYmJlcmhlYWQudGs/b21lbW8tc2lkLTIwOTM2ODE1NDU9NjI4OWFhM2JkOGE1\n" + + "MDFhMzYzMjJhMGY4OTRmOGQxZDk3MThkZWQwMzYxNjAzOWYxY2Y0OGIyYTQxZWUz\n" + + "NTkyMI8UgAAAAAASAHRwcm9vZkBtZXRhY29kZS5iaXp4bXBwOnZhbml0YXN2aXRh\n" + + "ZUBqYWJiZXJoZWFkLnRrP29tZW1vLXNpZC0xOTkxNDE4MjA9ZjRhOGZmODQwMDQz\n" + + "OTNhODdmNzAxM2M2MDA2NWJkYzg5YjExNjllYmNmYjgwNjBjNGY5NjY5YjQzYmEw\n" + + "YzgxNJAUgAAAAAASAHVwcm9vZkBtZXRhY29kZS5iaXp4bXBwOnZhbml0YXN2aXRh\n" + + "ZUBqYWJiZXJoZWFkLnRrP29tZW1vLXNpZC0xNDI5Njc3MTI1PWU4YTdiMTIzNmI4\n" + + "NTBiNDY3YTUwOTJjMGJkZmVhODZhNTNlMzY4NDI4M2ExZDVmZTI2ZWY1ODcyZGQw\n" + + "YWVmNDFIFIAAAAAAEgAtcHJvb2ZAbWV0YWNvZGUuYml6aHR0cHM6Ly9jb2RlYmVy\n" + + "Zy5vcmcvdmFuaXRhc3ZpdGFlL2dpdGVhX3Byb29mAAoJEKAn2y8+HhGKwsUP/1o5\n" + + "+7BMfta1gsVSEBvaqmCZDK0jL7Mo3g2Sayiw+aOVyFUIYy//YLd4QZGIjn7Wq015\n" + + "pjA/sSwAEtZ3rUE74ACbi29YMqSqgfMBvuD6O3u2TvV0y5I6ozGUkwP2cicNlXxn\n" + + "cONKBpfDRGa1VDIg4ghGM7/Al4AaBMIhNAQOJS1FiofXZ7qJ7jKK57BY8e1uUfg0\n" + + "KChPv/xu21wrhKy8DusBz7PSt8S8KBtisst8Mq+ew8rLRFbZ0F/l5VgvdudVSaR1\n" + + "mmSToRvmKgi2RHjIs7hlEEwRr+dWGO9SaW0oxNbVygMlP/pLEn1R9U94tAxDLXgm\n" + + "aDYL2NNXwyka5uBKLsy1dHXqXukKPS8py2PZhu2FJMLU0+ml+s2kTbA2Bze7slRO\n" + + "uiGPJg9WzovCQYVDam8eafGDMC6Q393HXH+gxq29LRg2Lulf4NJtosO4JVbOyzee\n" + + "Rd5FlZkUiJ7vbiVqIzGN8jel8Mr/NNKCcockwmry1u3JArwgNSqR+Uv+CeH446bm\n" + + "lfZ6JrwKWQRcuKVRfrXGuT46YmoFSaJjjlTATUVxcUuQNkFlQ6bibmdzEmFaKpVS\n" + + "QUf8gXnEjLh78K7kdx81c9cmIU4GrulK2uzGQULt3UgKytyrYf5EOwqnbrDDhAYR\n" + + "FwclbYRjvPUZSlTWoCo4u72gOuxdRWDgya9Ic0YnwsPIBBMBCgJyAhsDBQsJCAcD\n" + + "BRUKCQgLBRYCAwEAAh4BAheAFiEEf5EW/qkKWYOTbHz6oCfbLz4eEYoFAl/II7ZI\n" + + "FIAAAAAAEgAtcHJvb2ZAbWV0YWNvZGUuYml6aHR0cHM6Ly9jb2RlYmVyZy5vcmcv\n" + + "dmFuaXRhc3ZpdGFlL2dpdGVhX3Byb29mkBSAAAAAABIAdXByb29mQG1ldGFjb2Rl\n" + + "LmJpenhtcHA6dmFuaXRhc3ZpdGFlQGphYmJlcmhlYWQudGs/b21lbW8tc2lkLTE0\n" + + "Mjk2NzcxMjU9ZThhN2IxMjM2Yjg1MGI0NjdhNTA5MmMwYmRmZWE4NmE1M2UzNjg0\n" + + "MjgzYTFkNWZlMjZlZjU4NzJkZDBhZWY0MY8UgAAAAAASAHRwcm9vZkBtZXRhY29k\n" + + "ZS5iaXp4bXBwOnZhbml0YXN2aXRhZUBqYWJiZXJoZWFkLnRrP29tZW1vLXNpZC0x\n" + + "OTkxNDE4MjA9ZjRhOGZmODQwMDQzOTNhODdmNzAxM2M2MDA2NWJkYzg5YjExNjll\n" + + "YmNmYjgwNjBjNGY5NjY5YjQzYmEwYzgxNJAUgAAAAAASAHVwcm9vZkBtZXRhY29k\n" + + "ZS5iaXp4bXBwOnZhbml0YXN2aXRhZUBqYWJiZXJoZWFkLnRrP29tZW1vLXNpZC0y\n" + + "MDkzNjgxNTQ1PTYyODlhYTNiZDhhNTAxYTM2MzIyYTBmODk0ZjhkMWQ5NzE4ZGVk\n" + + "MDM2MTYwMzlmMWNmNDhiMmE0MWVlMzU5MjA+FIAAAAAAEgAjcHJvb2ZAbWV0YWNv\n" + + "ZGUuYml6aHR0cHM6Ly9mb3NzdG9kb24ub3JnL0B2YW5pdGFzdml0YWUACgkQoCfb\n" + + "Lz4eEYpSJw/+MXSg/xXIpdIVQ3NWeWB3p05op3/ilfb8GuF09XGqck4DeUq6aj93\n" + + "LD997vFmvL98ypGoyIpe3ds3DoUXzSFVjPLttFcHPsNm2CmkK6L9M1MY/2JzIRPh\n" + + "9GO1fUe5ZxspXgsf3rTZmkYXRUi/22DrEODm6H6fSK57D9J90ppRe8Rm8rqCV29J\n" + + "ht1LLgiaCwbz/DKQBBWv5ePaesnyYGTePWqLeHCLsa25mX46NS2HlBSFrcmyR+58\n" + + "wxnhkXn8SAbm5JCu/FpY5KX0PSY71QDfPUN2BaOoHRHRT5mSxqsgInJHnRVDf66L\n" + + "Lxh65LnfpdjCjTUsT6WPu7DgO9F8ObYnkno+YiaP7b9Uz7qzV3eK8SwmWTLiGE/x\n" + + "E1wFGGSvJuvCFAsMGvnc6lVZGA/F3jJCOdBy/QwyfVU54bGjPyUmodZAKoxn6Og2\n" + + "jylRUL3a9zdzt6sRxeCtuY+eqbq0ZcasP7b3PjYyMOdNQz2k9G0Fz7SW4PPQiu/m\n" + + "JFuCV33O6X5boaqoO/HTa9ZLJqCA8DjbF4i4r2phVzlv0veskqY1Nl9myGV5Mfq/\n" + + "El2dc/WfjlZAaw5Hs5qz9vdeFgqu14tSZVdLGENtg4F4TFdLobTE/ElqAVYxyA6e\n" + + "PsbOVHdIrwnZQmQvDJEoZEumZbXFmDhAm0Rb/9J8kqAd4KH0wIZq/VDCw38EEwEK\n" + + "AikCGwMFCwkIBwMFFQoJCAsFFgIDAQACHgECF4AWIQR/kRb+qQpZg5NsfPqgJ9sv\n" + + "Ph4RigUCX8giNT4UgAAAAAASACNwcm9vZkBtZXRhY29kZS5iaXpodHRwczovL2Zv\n" + + "c3N0b2Rvbi5vcmcvQHZhbml0YXN2aXRhZZAUgAAAAAASAHVwcm9vZkBtZXRhY29k\n" + + "ZS5iaXp4bXBwOnZhbml0YXN2aXRhZUBqYWJiZXJoZWFkLnRrP29tZW1vLXNpZC0y\n" + + "MDkzNjgxNTQ1PTYyODlhYTNiZDhhNTAxYTM2MzIyYTBmODk0ZjhkMWQ5NzE4ZGVk\n" + + "MDM2MTYwMzlmMWNmNDhiMmE0MWVlMzU5MjCPFIAAAAAAEgB0cHJvb2ZAbWV0YWNv\n" + + "ZGUuYml6eG1wcDp2YW5pdGFzdml0YWVAamFiYmVyaGVhZC50az9vbWVtby1zaWQt\n" + + "MTk5MTQxODIwPWY0YThmZjg0MDA0MzkzYTg3ZjcwMTNjNjAwNjViZGM4OWIxMTY5\n" + + "ZWJjZmI4MDYwYzRmOTY2OWI0M2JhMGM4MTSQFIAAAAAAEgB1cHJvb2ZAbWV0YWNv\n" + + "ZGUuYml6eG1wcDp2YW5pdGFzdml0YWVAamFiYmVyaGVhZC50az9vbWVtby1zaWQt\n" + + "MTQyOTY3NzEyNT1lOGE3YjEyMzZiODUwYjQ2N2E1MDkyYzBiZGZlYTg2YTUzZTM2\n" + + "ODQyODNhMWQ1ZmUyNmVmNTg3MmRkMGFlZjQxAAoJEKAn2y8+HhGKNEoP/j/1WwVN\n" + + "9h17ZpRvz9ZD3e9WN8iwYZnGTvjuAuCJPpCznfEOszP4Gcy/ixPRQXrnAYaqCoFL\n" + + "08a+dambbFPhGduVgoSkItwNcl6KyPv0Q6dDykXfKXBHSTAdvMmhhL51/f3J0Sxa\n" + + "xzJ8ev2OzOqJIUzkXtRHwYrdrclJrX/iLankL1lBZzDXJwf+IpAPczBe2a/S/sYz\n" + + "NAeiH/OvipNpchQQG6lkQF0duzdx1OudbIGQYuWzVtep8uokIJcrVxMleVZLEvfF\n" + + "ZkR5woOSloTbMfB/Kb1MEL2w9R8trJ5F81ZvXolL/DDIEqLTbZEpP9pUSYLe+eWO\n" + + "6cszMA2jBwIPYgamg3JKXS/zdDdqnV++rF5/IztLAv1T8mqClOUstU88LuYGBchu\n" + + "N4hLgeIB+/q0EmCTWIM/ewnMh/KEZHVXJI+ljoMjwS2dZSkV8KcmTVtU1JccR0Ud\n" + + "HxpYtrcwZaUgzFkPJf3WvFidt7rDs32DJCZiM/NKhzIdukyvG6DAFzabveR8XTlz\n" + + "evZDNn6gVw04v+jtX++YbasBOI4t8wj2mtqCs3f2bLTRx1fuBu+DyNjAbJT+925G\n" + + "kbT4gBkb/8aJOOvwWsT4ljXqngXykk39eAVSmRNlsa2Wnv5v/5Fh4gGY5ecfdLH0\n" + + "z14X/fFoVPkp+g+FOD9lpEP96swULamszCG4wsNABBMBCgHqAhsDBQsJCAcDBRUK\n" + + "CQgLBRYCAwEAAh4BAheAFiEEf5EW/qkKWYOTbHz6oCfbLz4eEYoFAl/IG5qQFIAA\n" + + "AAAAEgB1cHJvb2ZAbWV0YWNvZGUuYml6eG1wcDp2YW5pdGFzdml0YWVAamFiYmVy\n" + + "aGVhZC50az9vbWVtby1zaWQtMTQyOTY3NzEyNT1lOGE3YjEyMzZiODUwYjQ2N2E1\n" + + "MDkyYzBiZGZlYTg2YTUzZTM2ODQyODNhMWQ1ZmUyNmVmNTg3MmRkMGFlZjQxjxSA\n" + + "AAAAABIAdHByb29mQG1ldGFjb2RlLmJpenhtcHA6dmFuaXRhc3ZpdGFlQGphYmJl\n" + + "cmhlYWQudGs/b21lbW8tc2lkLTE5OTE0MTgyMD1mNGE4ZmY4NDAwNDM5M2E4N2Y3\n" + + "MDEzYzYwMDY1YmRjODliMTE2OWViY2ZiODA2MGM0Zjk2NjliNDNiYTBjODE0kBSA\n" + + "AAAAABIAdXByb29mQG1ldGFjb2RlLmJpenhtcHA6dmFuaXRhc3ZpdGFlQGphYmJl\n" + + "cmhlYWQudGs/b21lbW8tc2lkLTIwOTM2ODE1NDU9NjI4OWFhM2JkOGE1MDFhMzYz\n" + + "MjJhMGY4OTRmOGQxZDk3MThkZWQwMzYxNjAzOWYxY2Y0OGIyYTQxZWUzNTkyMAAK\n" + + "CRCgJ9svPh4Rijb9D/9LGdGSSD7DhHEd9vMKHe7PL+pysg2K/aTm+XMHKozCOkaf\n" + + "hnF6ltSW5vjCXOaEnMtKpH5vnb/RL4tKuWLj/CVC0L1rGAa0MQ0b4AG4fWlbctw1\n" + + "I7PAEES+fUFLftrnMgxYF97gM/yGp9a74IfIKHcZ+sVs7dw9Sa8kDCtg3KBCFG4h\n" + + "Y5PqUDVlQjWDU0E17y7Vx+0yT9Gfw6esDoao1vCGJhe+AZRZdr5fasdkejUhnZEf\n" + + "We1NhGbpfQSh92blSu8YxDhM1N0JFL2WOpZ0JVi/N5rYBRsh9gxHSRhsk1xu9EMU\n" + + "OWORX80bBrvN0md8N4F2SWtuzOz/CpJejrxqvx07lJSW+2nRA9TESg1vdPQxhlBz\n" + + "NL4HgixHxMhURjPYcvNa8ZPMC40aqukAwt7s/JVMpGwAqOPrCZX3afsbp/OOX3Jp\n" + + "F5B0V15GNuoDww/yIGAl+7x8QA3L6eDfjgEHtVYKKJEWN/SHak6QN6M4/ku3zsxk\n" + + "gguhr+hZ+BPXh3+Pk1NGiAKBpo+nnUKBcUpXWV4ie1E8DsNhLGgn2Gci2aTt+CW8\n" + + "LboPCZJGokhnQhimElUbgZ/8ggsSC6fYqA6qe1EONjw0TSerMJETZqeH/fwJURWN\n" + + "BH9Yo+cWM7WqG3p8zy1s6ztBMugvZaM8I4C64TtNbjjgwP04lqrPxtvADoZepcLB\n" + + "jgQTAQoAIQUCV/QFEAIbAwULCQgHAwUVCgkICwUWAgMBAAIeAQIXgAAhCRCgJ9sv\n" + + "Ph4RihYhBH+RFv6pClmDk2x8+qAn2y8+HhGKs+cQALvEPLUb1jqTtMb/grxritVy\n" + + "37BKecW5rMgIXnEPY19PU7fpFtehnAiX1Ydg645fGoDuEqSQKXxN3vOpW7RytNgr\n" + + "JnSB4/a4kJFWctQVbknx99PA3LienS1YcEa48uYTz87RKwYVE8PDVCxqL0+8Q1JE\n" + + "eaR6xWZfumBJMyBYN5yxhTn2BOQbU09WUwR/lqJc+0Ig1qGSOhpN08MoCqQTpUkY\n" + + "S9JQ5+jEYfQq0G0FdI/5SoSBB0qS18cs6mkEAwYtQq6DMH54KcnHT1gBwnBgZgPj\n" + + "J1rclBjVrUS+fBzNznZCXRTFrx8sWamYjeOeJjoBbzX8zpDANqJOwrcFX0qCzVJ+\n" + + "K7mGIPBQKpDYIV91b94xO0Unp8YBylyQ5WglfLdYlV2wMB9eNkayJHblhQtiQggE\n" + + "zBfqco+QlhFZjIh8pSON/wnpWlOE0gPUrcHIWkAWxIatPrASpYhQb+I+Ewd6XsJH\n" + + "oiU/3J3kojdJIoCQgdN3mI8cdORdq8YW0z6ZvFuC9nZp3TW28gut2aDYG9/EPqpW\n" + + "rLaCrs5qTxiikqd7zEsrexy8roMvr27uCP5gjKXYzNK3NL/xHnignZuBiAM67mNF\n" + + "jU7wrZHtnFfOPGnuubkpaPS4hMYkZnZc98ELV352sqFyX3dAvnLVHBqzs6SpPMyZ\n" + + "f+mHlHyFlkU8IXvPmhByzsDNBFfz3OgBDADWLIoatRXvo51XQta+AYScGlwnlB5H\n" + + "oPnwLfUdE2rly+8zE86omWM24Rf3bBUOwsCNxDotDyupPFJwB5lc+RmFg3AfjZDe\n" + + "jvr2GEX8CN603z3VuxVqVoUI7uPy+X0UbD5sh6vUJ+SkVBzLejKFWQCvQnVo+U8N\n" + + "E46lDEIzzWRSr8PSzTUU3ZILbExXb528wzIosaS0m+prGbQJN8jBw7l350y/uqX0\n" + + "4/NtTGE+x2XJyhgM/jnKzyB8xiY+SYHoMhD3yKnT8uNIHSzgg4fzGCpNGxqR5Zfw\n" + + "ZX++fCHaog/Uw+j0XvogTTadknVJINkf1wccLzwPhAsre1beIBaac7peMW7yKF4t\n" + + "TRbTNje2Pjz5A1ZdmITTXL6L/DrUFpaXXukCnfj+AQF1uoSjLzpvJRdalpr6OZhw\n" + + "HOQrYHbGAeAPh/1Np5m0dWYyidqt7GKh4e66g6B+TJT+LQN5XBy5iyCLiBB6489a\n" + + "5RmCPXehNm0fOmUY3thmR5tvg5Dn0Z0GxcUAEQEAAcLBvgQYAQgAcgWCX/iQNwkQ\n" + + "oCfbLz4eEYpHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMuc2VxdW9pYS1wZ3Aub3Jn\n" + + "v215/4oD56DA4iFieGJvYIv+F8ITuV/t/0aycU116SYCGwwWIQR/kRb+qQpZg5Ns\n" + + "fPqgJ9svPh4RigAAN38P/A9xXbD9n3BGT7/LdAbEkhsUDoLR7nB7nlT4lClgGITk\n" + + "u2rEGHWjsifzV+djuPp+Kf+cWXWVOdhL30AFiYPq8hZejO41npx+H8tA+dIIe22e\n" + + "mP7JW9IN5K+CRL8XC3XTLnel4ZHAt1z2/ZdXO4bAuU+gOAVNUxOzh2ytbgKex5w8\n" + + "rtJt1AlaNOVcA7OiZ6OaFIBiPsFaF7ZXYPJlF1STE+2Vwzixb6zr9kZf0lAkGA37\n" + + "9mxxD5hxjteakAe3bltqJH82XfIaJ03u7sAcZLHthcJJYDiibtAsfzt+nsLxLvDy\n" + + "uUYx8WmqV17MhvqK0pnYRKk8N6U02XRJ4HaG5X5AkyLqYyYeF1QiOyGFRhp7hwwL\n" + + "4vQ7RaikFN8xXsj1YviCERjE9CqtZ10cKccEFCHMllR264SeugiKIzf+ed/3ds/i\n" + + "+Mtd72A8Lr2NIf1vyQ1BzIVLlDhZnRmrmJjgXduwbhhNalayY0lyjbUnc8tSYU+D\n" + + "V36cCuas1HOTdRoVsCfamIyKLDxQR6hpr780WUX5lTdVS45NGoXfUVcRlUvWN7PD\n" + + "0fknLO8AdjCFCN8MIfk2jSXDgMi8VPI2AUoz56YnSpQSEVbvtAMKcIhM/5ObqJoY\n" + + "SNExTUqr5bkiEIcujMrdmPczcrDJtoOyEtlRBJhKRsUY47B/lNWGu3v8YtQ5mWt0\n" + + "wsF2BBgBAgAJBQJX89zoAhsMACEJEKAn2y8+HhGKFiEEf5EW/qkKWYOTbHz6oCfb\n" + + "Lz4eEYrgAg//UvHWgBE+nOiW3u3VjwN02OzmYCDk7WUamHqiEc/oxuYcOywMGcg8\n" + + "XOE47FMWknW5KBJ6DVFuLPd+Ugac/ap4xeZ0KcWnpomr04sgdJYZcNxuJEqTloWS\n" + + "zMuBU1R43D2KT6f+4tH+LIQ+siitqROoFJhjJEdakWDYamktIUsvX9sW7H5ZqmVq\n" + + "Cb9mDLc53lERTsrg7Z8abGWTgp8saXiepKLz/9A0fAgV+4NSAahQhjGMHaIhbsJM\n" + + "jv28ltUHophE0U1X9pIOQLqIDKFVLhSKTmwzKbZGAeFDvnLyn+ARFihC2nB7Ik7x\n" + + "aAut0Ws1v6uZ1is+VoLgW8QHggxfKI+m2kVxfkrPugrVt+Y9IyQSKWvspxm/AnE0\n" + + "VZvtz8fmo38fLtvlS3qiqLVB8V0iFGhPvCNH2K9oViz0Zvtk4Q1dIhoF5V9NtA2B\n" + + "S/J/gC5DDZAxRh9fFj4uEsy2G49Eod6YZGKpRYICWmzbhOeA6f94wY2mT3QQpiWy\n" + + "PcJR2O5yRX6+iVZ9lYANw75104dqaht7uhyCxLZ8OHk7ujBBHFqJubeOgMNnUMFh\n" + + "yl3NsPRb2vVuCJ+SpLf4kn/NjO4fTy6AqU0BP+LvhD0bPF3sIIBo9tMHXHBV8X6X\n" + + "op9cn4kDuvTdxjASZGce4tOVBh4KMhyOqAdeqnfFLZEHt5UGp/of8azOwM0EV/Pb\n" + + "PwEMANvXotph9BCyrs8NTj1zmaxOvygrc/6HZvb+JiJDaEonyjPEgLoKDePUgdz+\n" + + "kuWk6d8cSpOm47vBoxf5emVry82htPH9nIGkUyhfFRZkxn7HZ9KIcr+c7NXdBh9M\n" + + "0Ig1mWRj6bYOJqJHBpRZm+fV9T+CzGlg05IdBv6dFKTSjAv/pjIkAfhuvNEhNGLO\n" + + "2m/48QeuDzsHjjM80/+V6zNSy4SYw/hPGyTSoU1yJyibtLYP8rRN7x0+qqx3IiyB\n" + + "NuWZrH6Du6AGffASdk75UiEGr7UVf5ysDx/mBLFMdBoOeSyEHTeypdlC7e8Az7T3\n" + + "fzEI4+0ibUEV7+EH+94Azn/AVa3vt6WZ+KFgImy6CBM4S2GQmetvTGYRMXosXSzk\n" + + "5twraPZQoUkEEYy08/4yFEbWBniM3nA472rwXDFyjYxx6UZP+wZ/gaqrQKpgKl7G\n" + + "Ioe1VVq2bvQpbbWg52K4QpyYmubvfXnGqbjJNXDQN40fK0jVoH3V8pw0czpN0NRA\n" + + "BgJhhQARAQABwsMVBBgBAgAJBQJX89s/AhsCAcAJEKAn2y8+HhGKwN0gBBkBAgAG\n" + + "BQJX89s/AAoJENzPszAsnkYVdRkL/j13BrBz0MTnRdYO5Ljd9sN2ryLB1EZFyXqJ\n" + + "YPZgS0tzy5hWpRvSxH2+N9F3d09LbKLaihuGIApv1XWztIPEhVCtzclIq5rylbsb\n" + + "flr8yQ5iL/cI4krQjoV1Z8BYhR6rD97UbPXC+yrhmtnJ9YgL/WSivZqIDv3WOHVW\n" + + "QzlMoLZjBX7hawVODes5MiSkFep+P5s6O7uGLYEwKU0Ss1ohBwFBCpCUlc0cftLX\n" + + "h8Yo6WxwVRXcsPl0v7095reC+RZtG8DBS8Rhf/of2DOqyQa6qSStIfzjnxQGjWt3\n" + + "+TQ9RgWtOqC6/wFy5zk818G5wp4nOwcjBnnlbZGKYJqJIWS7BGf9FcVYxzIb+UcC\n" + + "dQEUB1YX86slkYbznicfsRMvHo8cXGE37wwQVgJ2cgToUQFvMmE2T7Qxz5+5II3v\n" + + "EXm4lFle+HFFG+rqZXX9S6kgJlm3m+p16GCqV0FX2+9Yl3gKbUFLqgg8j83YagpH\n" + + "wmteeSTpIc8UttQq2NAw6mLnPFnphRYhBH+RFv6pClmDk2x8+qAn2y8+HhGK570Q\n" + + "AI9PhyCeAM/Wiq+TodmE85C5L/U6/qS4gSQrqRewD/57fA9O59bg1ntEyz8QW9uH\n" + + "3Q/5fiE+ck7KI8bLOY6zzC0hTYxszwkgs6hTfRe2z4P6kPJNMyRv6iFKSB0nnA+K\n" + + "4fMcWnnsGOkA6b97weeFJ5effmM2WCuciPIwf6XMjeewCvyCmZ3tpTlt7nbJ5bVC\n" + + "QZzp5Dsc5p58g3cTHvomYIeVsojD00kwYZyzRohfYOt+nHWrwVo4/WjNJQUpw+oO\n" + + "UGVgCqgVgCYTUomzREMaVo8mFe5WR2mo3x7M9DoSfVzALt6qdyJ8lkj0FJUKftJl\n" + + "WJ0WR8vRXTtwThL25LZ0tFr2drZ99lql+TB4qg8121laRu+GPbWdCQZGH4OjVu3b\n" + + "ozElF1TceCefGGrk4cDwD46e/pfQVrE7b2oMXx0b0519oQP0YRyTSW8vssgnWaRa\n" + + "lFWwDDgwBCCB4LbYrlnyauuRvo4KjozCP7ZrzAVbni6i6VSUWIz27FoQsV0BFHLG\n" + + "42P2fdiPZy5e2CJUuG5XQOkK3ndgZgZYnOJW6RuKB779VY7gia2lkyZJkXHvHGTx\n" + + "+4glDEAm/O+E7IoANwTIE6N9umdkRk89qtsjSYuGLzBo2yMk80k0MFqQNRlHxskZ\n" + + "/BE7tydXnOpXhGvlOawIWgfXGwDI0HQhVgdHpPoXl21+zsFNBFf1A1ABEACpA1uS\n" + + "uwl+3+a6zzwWsvjRAZnLfAe0ibEmvVMoqF/y6m+VDmivoXFEC+a5fCc6qEwcVE1B\n" + + "AZqvbvklzXhwu0jSrNGKz3Vr3FlwtuS0h6W+EEWTh9B0Y2bNiyB3hqRnZ0KuUMUu\n" + + "gIifY/G2TPDN3FhCWiU+QJcpTazO+Up74y3YXLxgBo3Zt4H2xf0EzMH9nuKKKtmA\n" + + "pQTHMnUQu4Bd/AOrWYJgTQqlFwVJZAcggjLcyk5p8QMGyKpwXpXagvwqHgA0Ct+B\n" + + "YSoYkIpVyywQaUS3PKIeEjp5kCuv5iNlZMDv7A6cHASUqsxpjljEyZ/G+R6S9t+4\n" + + "7zCNhOqYpAOHrfmXzLe70OtEt91gqIoA6RXeBgBerV/CPuenAjQKQrlcTrlh4/hO\n" + + "xj3wWfb+HWiatz/rHQI/gN0oZi1Qg3xuaO8VhCQ98MgTszgU1/K2rb54aI4Ar2h3\n" + + "wajqd+421sGxAe/ftbT4ckHkrCgI0j8t0LPvtoOFjpqh2zMbSRjPRzT6ClY/nYPK\n" + + "ryY9PZ+6mi8suQpdni4szGKLdIEkloaZNJPwrP7R2d5vQNwyti9qClPeqJjCl5RW\n" + + "4zj1GP3ZbVUgcFG3FzImTmdyEd4Y9P1hvPa0CV5W+kyzi4P4VeXK6Zk7CcpTu3Pk\n" + + "2VgfD0qhpYcVQLBqNFtPHl665cRJKMxdore1cwARAQABwsG+BBgBCAByBYJf+JA/\n" + + "CRCgJ9svPh4RikcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5v\n" + + "cmcpsdpfx97DwXxPD4xmjMokBtXDs8EKClmOHnHi9CVxUgIbDBYhBH+RFv6pClmD\n" + + "k2x8+qAn2y8+HhGKAAA3xxAAvnU620zS2T3PIPYB2VMv7LoUJfAihwWB9J8L0DZG\n" + + "tS5GGlFiKQrXaqHpn0cdbm5UrcoXc4gNq78Xn1mG0c3osCmXWqSU/IWovKIubeyn\n" + + "UfDofUEk3UTNAHOJpdzryclmib8DwueFOKzEWyRVvpfW7FcGj0bD8QBOJ2LgjjvD\n" + + "gp6jza8RXOwXmMkAUU1Hk9QKJCmLgR5xZ+Jdqcb9iW6rn5o8NOOu4i/FPB0pIT9U\n" + + "3vXrYWw25uNtoeL89o1xdbFvF+cMOd50x6WtpbAqXtyoSo0giM3ANrY+f9afZBRa\n" + + "JGnRVdgQoSdmcbLXIOUe1Xu3h4eR35t1i9hGc3krwdz/534Jrqk2MBN0UOFhOPG7\n" + + "3uJnldrz5YkDH2N9/n6GoMowzj+a2p7VmqvjN1v0WxJHiVGjydaRh09ucAkBWPv4\n" + + "fOARyZolt6grFP4bLYundxaF/i2XsQDhU3+fXN0PMfkV0EcJT8oinMX3KAknsWLT\n" + + "1iAWnniL8N+9uYNjKTucmyvYZrNIQc+7mVBPZRJDripistlWX8iFFpJxvf+IOOSE\n" + + "4zYQGe0LPMImDORQT/8LmgJuwLxvkTfL0QJjEwN6QcDWepy8UforgN3giHt2VZbN\n" + + "GA+z6+8cu3+wy2phh1tUkQ2XvaJ6x4KnmOR7/Uoio8pTQx6Xt1K0CpXFO7byiJPZ\n" + + "957CwXYEGAECAAkFAlf1A1ACGwwAIQkQoCfbLz4eEYoWIQR/kRb+qQpZg5NsfPqg\n" + + "J9svPh4RignwD/9V0MDPMOIOs5TCsn21ww3rzi4tjqZUdG/B6eX4DEU2BzMUvr5K\n" + + "9Yi8NUf65ua03BQD2PMYWqnGafkJkZ5URAY7iaV7WvJ0SNlJuV2HyGbzqxStiaXO\n" + + "ntwvGxZpOO6nvg5/uEBtkuzpMG+8716J/MSfyfj2NtdZkMi+2k8PmdK6jvnSsmAP\n" + + "iuCeP32dZSgnlEa3xiFUkNdpQQNVCnGSNWQ7MpHgl1L94qtv41kGT8LI1b8K4R4l\n" + + "ovpaKleCCBW9UbD3btHijMLfHy7Ivv4Pg3mkSkEq9uVeNJDkkM2NG7R0dBiclvI5\n" + + "xVupf2bIHIBqSo8AaMsEFnfhgEcHPqlCErnN+O8PyrVSVP0LwaXKF9mtez2vpd2H\n" + + "4vhFnHbVKTmVIOsW4B1Mbxhnbvl9CPfqNV4uT+4Vg5WS+XmB+ZYWNIJ6JoBp1fJH\n" + + "2jUawrQc4DzJPr7ihdeKXd5L+UUo1VmfDRkQvz4Frcwxzl3yg8keHLJd6EvssPtI\n" + + "0VU5kAgTbmHkRf9vX/4dCvcyk5+PAiSE1A7Xq3uJTZ3FjxXCxEPSLHjM1GCt1+Tm\n" + + "0pnZVp2bH+jGLmgvoRDGEhmYEfzlMra+7fFD00C3UcbSQDNURs3MtRZzv8EkLLAP\n" + + "RA7Wcl3XI+M5pFuR+aatDz1hB1uFF/NvnvkjujzTysguoJhU2EKWTtIJIc7BTQRX\n" + + "89bnARAAs1NzkaHRNHWu2YiQk8lTctciFjyMlVH/Vy28yZSfpHWrt7MCzhkaK1PY\n" + + "sWlnJifOlCnvzyDW26ouLqbPR51lzRFs9UID1dzg4RCuPMs0TwlIfcUCbBRc3lq3\n" + + "An941sEwD0+gguGog1oIum2regAftnbSoQj/1+OoZZz0zqeDkHorQcCDTc3EfYsL\n" + + "jswiFioioOPWgPjG6DSa39xf07YdrW0DOwpJ/M+MCVoPxREqbXC/oCYUQ85h4V66\n" + + "a8YMYrmkeHzq1kuX7HXuoJKtX8W3vHCiPo/sU/wF74b0oDiskfeXwMaZoRhVPkYG\n" + + "BEIhAO6n9tqWtuSzxWmMWH/TDw8h2GM6hCa67YPVuiTnztNdr8FR9D3WFpcizpbN\n" + + "JFj6HBcrfO6IwD5NK8h5fiqFeIQAIfo1PL88OC8jDVjscF0YoJeCiI8sRFjP/1y/\n" + + "MbYaKIR4fA+PbogeW/klGeI8bp49dGQa+8cnrgDcnzNS1TXh1Zcaob9H+DDHdSCN\n" + + "37hHtfroFDBCr6KRQ55WzBTdR+zmibZDjkGY4T0uaQjFQAGshPNGcr63rCSWyZnI\n" + + "nx1H4WWwnsUquTt7T+qt0TAOfd+9shgPqz/dLKkkF87mBtS423dGdDp6BZJ5t4lp\n" + + "l8LGiSuk9p/ckoB4MET+1iLjaU+FECLFyIg95v6Gk1OYFxeDnnEAEQEAAcLBvgQY\n" + + "AQgAcgWCX/iQQwkQoCfbLz4eEYpHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMuc2Vx\n" + + "dW9pYS1wZ3Aub3JnYWATd/tucGd3FDiHb4AJdZ9NptbAcccakj1mpmFlMEkCGwwW\n" + + "IQR/kRb+qQpZg5NsfPqgJ9svPh4RigAAMB0P/RLNviscaB3Ii0ZLMs6wQOsYkJ/O\n" + + "1c4fm8ajmz+Z5lgEhgVbeFhmsqJHIgk/ni4UcdHAsKIBwfcVxZPzR+nH1g5CId/E\n" + + "2mZXCcCi586Z8jyn8b34Vx/rYdJVkqyBL3OtS+EOMBROvA5VsNWrIVm0BrGqzEjm\n" + + "0mKUuRldpZyjNRzD2beITJkOCk1H/Vqt+bCXmxb2akb+06bB0NqKG+kjRvlCnSwB\n" + + "vZ+RyiXVrOeeoj4ODgGyta0W+Rtnoa+AXpr5JE6uBc3Z+vgZrDndqBgD/SZUXjNC\n" + + "//Y3M6qxfji84e8HXmFzuZccmSzwH+Op6Mlh4XiPhqxmL5/AJoE8QxCUnCb4mENc\n" + + "nfGdmnlmWGrbApNkdmb3hXDXpZCJzRYgdPtUEuWJ5/mlm0979/bF8b0HN0eCG06V\n" + + "qoNi+nSNbeth7f/4gseq60DpcJaf4lGVs9AgjXFNGWSyZmHuFwM+UpfHZiRXfprb\n" + + "ilB7ZBhfhx9d0MZ7luyFAa1IdCjb3bpdvZMPSSx8xoW4obcuExVayJzJz1HCeWwr\n" + + "3wYuzV4SkWbSm3YWOBfK9EaGc2RogTu89esmyCwW2uslcSrnYS6Dp0lCcC1CXsKl\n" + + "ZKWV2GZgbZAUI+w7T71UT4Dw/P4qOvuKIfqvD2SJ0ZfoKoV1oa5YNHqpsJUmmc+U\n" + + "XmfTZgfP14xzBUyVwsF2BBgBAgAJBQJX89bnAhsMACEJEKAn2y8+HhGKFiEEf5EW\n" + + "/qkKWYOTbHz6oCfbLz4eEYrOhRAA1jBGY2Nrs3SRcou4Ih+4bgXMzG6qPRIh/2ac\n" + + "lDM86SHyrA4KrsVlsFiRWHndiyHnnqiT2BqX1tJr+FRqCkuzd5dsj3M9hLrG7aqU\n" + + "rJvoEAAo2A4NY1HofR3rpPbibNKEfkPSY0P9GV+8lzb0wKgJ3tzj1FUqjyT4Q3gm\n" + + "d9Va7647kHTFJG9Hmjzp/fUkLk4Fg9m3vBg7uaTe0LvF5cgfZk2WGRmqtmOP7hzg\n" + + "BwJP6fYzzKNeDyFnzUJr4Dba501wQ6YvmKWyh4gvnFNhI95oL9CqgnygsiHUjafQ\n" + + "WexpmXGWAlPvuUQrGN6352vSFf/g/t00sb+Ic0hp1kohOHsmJmA8BHZPHKZPLPO5\n" + + "/TvrO/VAd5GMm9iEHkOMAT5sWlnc1oNXHe7QTKpskgUrVjlOKCUkWqeP4Q2oHIVf\n" + + "2fUtSru0MoqqemqQvPfSzL8XvOnz35JAC/6rDLRWMmhA7bGhLi+K1dQrNH/OQbU3\n" + + "z3ZwXnlm8NhnuT2Ocu7A9jAfizdA4aHfTVTryzOoLMfO4qOYvmiJsBjnm9qgWMSC\n" + + "oC5HWI6sD3IxM5J0kgqPWpshyh0pQwvru0yffoP0iyC4Mti+v/5J8XXpAk/QUuRk\n" + + "Nfd1+cEU3U5Nej8jRfV8gDfe6VZ+7nfI1ALfPaiYPFF4CSb89XE7mX5jJ2cEA4Eg\n" + + "BlUanyzOwU0EV/UBfwEQAOBVbrr/emeHtuSpxTzNLq6WwLSYROYdhdQ486uDPKEJ\n" + + "P8S0Vf0OJ4HmX3rYQCFDb0zfZhS/Lu5mFx5Fg4oDGZ0Rlvh4HThnJJGbVZemj4f9\n" + + "L4p8hD36kaGMCWPBtgg+54HCuQY+pZvqGCuzJtw7K/QHB627ZAuN5xVXAIUXpMvR\n" + + "VUd3/H6qrSXvEQipVrpBHFVG1YcbX2erj31i5hwd7yGS1nJQfp6hwC5E/GbBWp/u\n" + + "n9WSKzttKAPls8G81GH6907pmvWvetxJaMqegPpB+tDJ1SESlnRsg7f40x9C7v7o\n" + + "S3DjyUGbN6AH/aNcktInC4Qly8etoFopsMjvxj/Mpl7NJmokjLmjY2KQxbkQmBiP\n" + + "Ba6Vi5ulRDzBA7hwVGGnqlNQiP6duNC5NpHhzCZbK8zWSlxB3NRT9KqV6FDezdFa\n" + + "Xm3rxNrTBl3u/NBP2Gog/0EBukirB7shSWISZ/S7d5MB+YrY4Zg5FRJfW7QwDdhf\n" + + "b3pwfr+T3TocttcbbExFk4lA0DNMlMyyhNpszxuEUp7rCi3S9Ushu+YIYO2JL0rl\n" + + "gZzGYLS4IbJmA9/mcxNi/Cl8fC+JVt7o0BNHJ5B8JXupGJN1JWVNkwYIdGoD06ER\n" + + "hPrre5vuoyjSXK0DRomx07eLi1bxKXHiGd6/GbaJaxToa2+EVIdfKPvoOa8tG7Gb\n" + + "ABEBAAHCxDwEGAEIAvAFgl/4kFEJEKAn2y8+HhGKRxQAAAAAAB4AIHNhbHRAbm90\n" + + "YXRpb25zLnNlcXVvaWEtcGdwLm9yZ6QrIsUofls51gJdZ+HVbH8dGKJVka/TmXp6\n" + + "YORSw2jPAhsCwbygBBkBCABvBYJf+JBRCRBivukmS/FzEUcUAAAAAAAeACBzYWx0\n" + + "QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmfFq6l7ZWZIJAuffcBu7/dVPtpCFk02\n" + + "rAl5iGot9GkqNRYhBGo6N12WZ24XXwA6ZmK+6SZL8XMRAAAWGRAAnpBgU8p/MAEW\n" + + "iHl0LSRV5pUWd+zsxO/FytOEe/ctDtHQ5+bEQLnjRuvsz1De2HuuMvfYFSymjNyx\n" + + "CxwfdmZ2UA/kCT0CvMo7tn4yhcIvyG/MCAnvERPDgibuJx+SguRhNa8Ld6DDduop\n" + + "EPyNKwzmnnitP0ar87CQ6sKlMzJH0dduflv7o+Bp0qWfUeagJ1ZDliFu/0AbwFYO\n" + + "bKJs9M5NFM0aa7V3xWWqVlTjQZA54O3ZlXcbYsnKHwmrD/tBTdyzfBBlMkBWG47b\n" + + "Og4jJN+p0L5/PQ3VzJgsGAdXgANbkopRTBvg8/+BqtvduOiug/ez3v/ywAkn9Mg6\n" + + "basjzWC1LLaH019NEwztfZRDVa/kbM0qt+pczra6S6Cr+mOlVm9cw+aHRB93QS2k\n" + + "MPf9EyliHBRB9AIrvcwY7imVgE1oMr2lDWK3G1OMrlGuF/6YBTI+Ok7lTKpIWpAu\n" + + "HD9yFK3BzfVnJqmAch54rpTtNZLOQXdFSgUBDjvj5CoQKJ/kdZW6wSNYUHpK8zNd\n" + + "lL1/pdq9EMQAyNDHN4/LlABLaZFe1Wy/Nn3bAdVYOpd5MpIYN++LyPALbVUIpARg\n" + + "M9D8LUWTv/PBAMeIvV2MmgrDVIY7wqCdS2WFRqmUfdfeZEPFkfJmNfhWb4YqIhg6\n" + + "NAfDnE34ddLOqv7FGz4m4Va4sFteztkWIQR/kRb+qQpZg5NsfPqgJ9svPh4RigAA\n" + + "i1IQAJPs9+RaZW6buMjkKO46eYWkq9/GtiRzp10XZHB3hreezadT4RHw8tqsm6h/\n" + + "8vprK+eJGfCsnhJz08XuItRZQYa1/XmsGTggQh6n2pmOjP7o1x90b4UdeRYi02k7\n" + + "wGCwGq6T+yi7jpjWMVEGRn28jumJQmfbC9PjsS0wBI5ne5QFyksu9PjtddYahcov\n" + + "1Kz/SUvwVUUgLh7TLi/Ll6qrLZb4tRlEGh4rkfM45dsAQv+ekLi4oh0lRoH33lH/\n" + + "sUSgwnzPyRVeqL8tcy70SkQ5295go5jt/WloXitjlQplbunG1gPQQxfSa+TD71ka\n" + + "2+wkSBuTyeWHXzi2azOv9gRkUoBOE341cBT16rMIkoIrNLy6uaXj6CtOl6fMqxQz\n" + + "Jl5wPYk6wccr3i8q4vGfiDZlDO+a8hLljIYF1TC4DThYTY4dwBGlv8dD6vb+Kj5Y\n" + + "FwhovXVsTzQboinToK58roVOM7TbQAN5OV47sagyIylaACybmEC2jYXmtxYpE3KY\n" + + "/pKdy8ItBlQRGdIbbS6cPtgUO+SQdjabHkXt6eg355kaXFV9Ci0XjJKsS0az1LXP\n" + + "ArU8qg40/7M8Bl/ytgmArsHJ4rEYQQxDNSdqh4oH0duSdoSATyyLwrwBYY3a2w1x\n" + + "V6jX6X3DjiiwxhciYto0C5BFX6uwQIJU6qcrI+qAKt6+rnnqwsOVBBgBAgAJBQJX\n" + + "9QF/AhsCAkAJEKAn2y8+HhGKwV0gBBkBAgAGBQJX9QF/AAoJEGK+6SZL8XMR13AQ\n" + + "AKDbc2MFkbJARAU1thZT8nbZMDNxhheaMe4M+1epv1FNxIP5kzxQK06rfwYAW6nf\n" + + "ms2Bg90FEJXa7KnZqfc5qj+eYPflLYrgcpwR4ZazXykI62RBSHgv9SUNmR9tEOL9\n" + + "jFd8Qf5x2qbYrCv6ElQmfLee0wrV2ML06nOzkwa71KnKfdCP6dOIa2VyVkQ9TaN6\n" + + "6yfGfO3qGnpsrd/vHs2Z17a8kTou7wt+Do13TZekbyLnIBG2XkDsY++KzWfNlO4h\n" + + "svAzyJKbVZfbtiiTwYZdtYWoImn5BQCUaYSGZdkkBgV/eqtXPoLzBieeKC5QTx1A\n" + + "bO5lz3xAD2iUdj0HN7Uzl4gutnJllLXakhagIRTY6rGSaiBKVhRBTb4ZwFmEB3DA\n" + + "n9Rd7C1+e5xhtoznDwENC7gCzr37fzW3VbP9rACs1LmMwQEBp8n+az591QDeNFKM\n" + + "NG9EqtG8vZZY4AER3s+6fzAGFehcj6hnWC7ZUjRpE1oWYLWUWaJ70crBY0X+14QO\n" + + "YcDCHd0GVg0alhayb/jbcVjUPqNisjX3RUCtKLlw4/auEv4/9fbX1jQogSfmNDp/\n" + + "gvDUdiUZ5fsq7GpFik2HiNtzITzT1G5QnHQpCwvwttwXdh5TfXPjcWy1OJsnQnht\n" + + "+X51zOWQnPe4NpayR0CdPAiFBpr+xrwwT0B6i3KJzKcfFiEEf5EW/qkKWYOTbHz6\n" + + "oCfbLz4eEYoj6w//fcy/NA+hsS9vJmsnQDqJbZMcWJZmImdlXl6MveXYpFR4iwQo\n" + + "EPY5E2y3N0hzhh4Yy0j9FG7SN3kKH4NysMQSAtgHI9E1sshGCBYSv3DNMsdbSjid\n" + + "mnGfEcFHl7uSITdhXDMDh84tfDnyF50d0y+Bcpdjb0ipLqDQzV/TISbnsuHC6IVO\n" + + "T8avF9+NQd2dXMCyxsLQziUuKoB1A+DloAz1HrpmZ5VJ9koebRIV8RIJmIV1Bv4Z\n" + + "18OBX2XCmWZUXudVMbEI+DTXogenj3j+0yYhalAj14rPndScRkr7NjR3sQxTzygT\n" + + "Q6k1YGQVitCSFdt90R/UHpBVX8a4bt4giM2ZYnSl0kjkyki7ZtAWMqmgdOks7643\n" + + "OWrS2IXgTUHy/oikwCpOvtUiV0hGtV7GhIbqecV+jshjXEko/yH/u2JFwjhEnEPP\n" + + "Njy3gUNb2qtRxqPSboKk+p9OFqGPNonzlgxS4KBRjC+2Lr9ky1VxWhIcsIjMMFxG\n" + + "STmGCJvwicAAKs1eAyxi3B5ES5taZmkdkZ6niPloFM44EO1cvEPKZLqiXOBlw7J2\n" + + "fzC53N9HXIdLdEkScbUzxSXQJ2e1nZ1U8pi+hOpRZqNqE+hoAxer9wVzCAljOSwh\n" + + "wMx2yRO5CAYzX41+jbqspsvqqX/YUCtxzbOAc1VrpvgK0jDlftyZD8q3aE8=\n" + + "=UC83\n" + + "-----END PGP PUBLIC KEY BLOCK-----"; + + @Test + public void longAsciiArmoredMessageIsAsciiArmored() throws IOException { + byte[] asciiArmoredBytes = longAsciiArmoredMessage.getBytes(StandardCharsets.UTF_8); + assertTrue(asciiArmoredBytes.length > OpenPgpInputStream.MAX_BUFFER_SIZE); + ByteArrayInputStream asciiIn = new ByteArrayInputStream(asciiArmoredBytes); + OpenPgpInputStream openPgpInputStream = new OpenPgpInputStream(asciiIn); + + assertTrue(openPgpInputStream.isAsciiArmored()); + assertFalse(openPgpInputStream.isNonOpenPgp()); + assertFalse(openPgpInputStream.isBinaryOpenPgp()); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Streams.pipeAll(openPgpInputStream, out); + + assertArrayEquals(asciiArmoredBytes, out.toByteArray()); + } + + @Test + public void shortBinaryOpenPgpMessageIsBinary() throws IOException { + String asciiArmored = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "wV4DR2b2udXyHrYSAQdA8GwHRf0XsR9FsPL36oNvdoBZPXddygb2iYdGBJko9X0w\n" + + "VQqhsjX54WCiMBQx4ma0om49rAWHCk4h4IAq5+WsdN+xCklAUXsbIA7BZUaXfzEB\n" + + "0j8BpWiU6SJ9YB23OtZSWl/5bu8hx1bnKd5ZM0D5VP2QF772Ci/oAGywSuOA+C6b\n" + + "G4Bkf1xlQ9vctnBpMix3xUA=\n" + + "=95Eb\n" + + "-----END PGP MESSAGE-----\n"; + // Dearmor the data to get binary openpgp data + ArmoredInputStream armoredInputStream = new ArmoredInputStream(new ByteArrayInputStream(asciiArmored.getBytes(StandardCharsets.UTF_8))); + ByteArrayOutputStream binaryOut = new ByteArrayOutputStream(); + Streams.pipeAll(armoredInputStream, binaryOut); + + byte[] binaryBytes = binaryOut.toByteArray(); + ByteArrayInputStream binaryIn = new ByteArrayInputStream(binaryBytes); + OpenPgpInputStream openPgpInputStream = new OpenPgpInputStream(binaryIn); + + assertTrue(openPgpInputStream.isBinaryOpenPgp()); + assertFalse(openPgpInputStream.isAsciiArmored()); + assertFalse(openPgpInputStream.isNonOpenPgp()); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Streams.pipeAll(openPgpInputStream, out); + assertArrayEquals(binaryBytes, out.toByteArray()); + } + + @Test + public void longBinaryOpenPgpMessageIsBinary() throws IOException { + // Dearmor the data to get binary openpgp data + ArmoredInputStream armoredInputStream = new ArmoredInputStream(new ByteArrayInputStream(longAsciiArmoredMessage.getBytes(StandardCharsets.UTF_8))); + ByteArrayOutputStream binaryOut = new ByteArrayOutputStream(); + Streams.pipeAll(armoredInputStream, binaryOut); + + byte[] binaryBytes = binaryOut.toByteArray(); + ByteArrayInputStream binaryIn = new ByteArrayInputStream(binaryBytes); + OpenPgpInputStream openPgpInputStream = new OpenPgpInputStream(binaryIn); + + assertTrue(openPgpInputStream.isBinaryOpenPgp()); + assertFalse(openPgpInputStream.isAsciiArmored()); + assertFalse(openPgpInputStream.isNonOpenPgp()); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Streams.pipeAll(openPgpInputStream, out); + assertArrayEquals(binaryBytes, out.toByteArray()); + } + + @Test + public void emptyStreamTest() throws IOException { + ByteArrayInputStream in = new ByteArrayInputStream(new byte[0]); + OpenPgpInputStream openPgpInputStream = new OpenPgpInputStream(in); + + assertFalse(openPgpInputStream.isBinaryOpenPgp()); + assertFalse(openPgpInputStream.isAsciiArmored()); + assertTrue(openPgpInputStream.isNonOpenPgp()); + } + + @Test + public void testSignedMessageConsumption() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + ByteArrayInputStream plaintext = new ByteArrayInputStream("Hello, World!\n".getBytes(StandardCharsets.UTF_8)); + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() + .modernKeyRing("Sigmund "); + + ByteArrayOutputStream signedOut = new ByteArrayOutputStream(); + EncryptionStream signer = PGPainless.encryptAndOrSign() + .onOutputStream(signedOut) + .withOptions(ProducerOptions.sign(new SigningOptions() + .addSignature(SecretKeyRingProtector.unprotectedKeys(), secretKeys)) + .setAsciiArmor(false) + .overrideCompressionAlgorithm(CompressionAlgorithm.UNCOMPRESSED)); + + Streams.pipeAll(plaintext, signer); + signer.close(); + + byte[] binary = signedOut.toByteArray(); + + OpenPgpInputStream openPgpIn = new OpenPgpInputStream(new ByteArrayInputStream(binary)); + assertFalse(openPgpIn.isAsciiArmored()); + assertTrue(openPgpIn.isLikelyOpenPgpMessage()); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java new file mode 100644 index 00000000..6102372a --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java @@ -0,0 +1,704 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.util.Date; +import java.util.Iterator; +import java.util.stream.Stream; + +import org.bouncycastle.bcpg.ArmoredInputStream; +import org.bouncycastle.bcpg.ArmoredOutputStream; +import org.bouncycastle.bcpg.CompressionAlgorithmTags; +import org.bouncycastle.openpgp.PGPCompressedDataGenerator; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPLiteralData; +import org.bouncycastle.openpgp.PGPLiteralDataGenerator; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.util.io.Streams; +import org.junit.JUtils; +import org.junit.jupiter.api.Named; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.CompressionAlgorithm; +import org.pgpainless.algorithm.StreamEncoding; +import org.pgpainless.algorithm.SymmetricKeyAlgorithm; +import org.pgpainless.encryption_signing.EncryptionOptions; +import org.pgpainless.encryption_signing.EncryptionResult; +import org.pgpainless.encryption_signing.EncryptionStream; +import org.pgpainless.encryption_signing.ProducerOptions; +import org.pgpainless.encryption_signing.SigningOptions; +import org.pgpainless.exception.MalformedOpenPgpMessageException; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.util.ArmoredInputStreamFactory; +import org.pgpainless.util.Passphrase; +import org.pgpainless.util.Tuple; + +public class OpenPgpMessageInputStreamTest { + + public static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: DA05 848F 37D4 68E6 F982 C889 7A70 1FC6 904D 3F4C\n" + + "Comment: Alice \n" + + "\n" + + "lFgEYxzSCBYJKwYBBAHaRw8BAQdAeJU8m4GOJb1eQgv/ryilFHRfNLTYFMNqL6zj\n" + + "r0vF7dsAAP42rAtngpJ6dZxoZlJX0Je65zk1VMPeTrXaWfPS2HSKBRGptBxBbGlj\n" + + "ZSA8YWxpY2VAcGdwYWlubGVzcy5vcmc+iI8EExYKAEEFAmMc0ggJEHpwH8aQTT9M\n" + + "FiEE2gWEjzfUaOb5gsiJenAfxpBNP0wCngECmwEFFgIDAQAECwkIBwUVCgkICwKZ\n" + + "AQAApZEBALUXHtvswPZG28YO+16Men6/fpk+scvqpNMnD4ty3IkAAPwK6TuXjNnZ\n" + + "0XuWdnilvLMV23Ai1d5g6em+lwLK5M2SApxdBGMc0ggSCisGAQQBl1UBBQEBB0D8\n" + + "mNUVX8y2MXFaSeFYqOTPFnGT7dgNVdn6yc0UtkkHOgMBCAcAAP9y9OtP4SX9voPb\n" + + "ID2u9PkJKgo4hTB8NK5LouGppdRtEBGriHUEGBYKAB0FAmMc0ggCngECmwwFFgID\n" + + "AQAECwkIBwUVCgkICwAKCRB6cB/GkE0/TAywAQDpZRJS/joFH4+xcwheqWfI7ay/\n" + + "WfojUoGQMYGnUjsgYwEAkceRUsgkqI0SVgYvuglfaQpZ9k2ns1mZGVLkXvu/yQyc\n" + + "WARjHNIIFgkrBgEEAdpHDwEBB0BGN9BybSOrj8B6gim1SjbB/IiqAshlzMDunVkQ\n" + + "X23npQABAJqvjOOY7qhBuTusC5/Q5+25iLrhMn4TI+LXlJHMVNOaE0OI1QQYFgoA\n" + + "fQUCYxzSCAKeAQKbAgUWAgMBAAQLCQgHBRUKCQgLXyAEGRYKAAYFAmMc0ggACgkQ\n" + + "KALh4BJQXl6yTQD/dh0N5228Uwtu7XHy6dmpMRX62cac5tXQ9WaDzpy8STgBAMdn\n" + + "Mq948UOYEhdk/ZY2/hwux/4t+FHvqrXW8ziBe4cLAAoJEHpwH8aQTT9M1hQA/3Ms\n" + + "P3kzoed3VsWu1ZMr7dKEngbc6SoJ2XPayzN0QYJaAQCIY5NcT9mZF97HWV3Vgeum\n" + + "00sWMHXfkW3+nl5OpUZaDA==\n" + + "=THgv\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + + public static final String PLAINTEXT = "Hello, World!\n"; + public static final String PASSPHRASE = "sw0rdf1sh"; + + public static final String LIT = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "yxRiAAAAAABIZWxsbywgV29ybGQhCg==\n" + + "=WGju\n" + + "-----END PGP MESSAGE-----"; + + public static final String LIT_LIT = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: BCPG v1.71\n" + + "\n" + + "yxRiAAAAAABIZWxsbywgV29ybGQhCssUYgAAAAAASGVsbG8sIFdvcmxkIQo=\n" + + "=A91Q\n" + + "-----END PGP MESSAGE-----"; + + public static final String COMP_LIT = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: BCPG v1.71\n" + + "\n" + + "owE7LZLEAAIeqTk5+ToK4flFOSmKXAA=\n" + + "=ZYDg\n" + + "-----END PGP MESSAGE-----"; + + public static final String COMP = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: BCPG v1.71\n" + + "\n" + + "owEDAA==\n" + + "=MDzg\n" + + "-----END PGP MESSAGE-----"; + + public static final String COMP_COMP_LIT = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: BCPG v1.71\n" + + "\n" + + "owEBRwC4/6MDQlpoOTFBWSZTWVuW2KAAAAr3hGAQBABgBABAAIAWBJAAAAggADFM\n" + + "ABNBqBo00N6puqWR+TqInoXQ58XckU4UJBbltigA\n" + + "=K9Zl\n" + + "-----END PGP MESSAGE-----"; + + public static final String SIG_COMP_LIT = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: BCPG v1.71\n" + + "\n" + + "iHUEABYKACcFAmMc1i0JECgC4eASUF5eFiEEjN3RiJxCf/TyYOQjKALh4BJQXl4A\n" + + "AHkrAP98uPpqrgIix7epgL7MM1cjXXGSxqbDfXHwgptk1YGQlgD/fw89VGcXwFaI\n" + + "2k7kpXQYy/1BqnovM/jZ3X3mXhhTaAOjATstksQAAh6pOTn5Ogrh+UU5KYpcAA==\n" + + "=WKPn\n" + + "-----END PGP MESSAGE-----"; + + public static final String SENC_LIT = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "jA0ECQMCuZ0qHNXWnGhg0j8Bdm1cxV65sYb7jDgb4rRMtdNpQ1dC4UpSYuk9YWS2\n" + + "DpNEijbX8b/P1UOK2kJczNDADMRegZuLEI+dNsBnJjk=\n" + + "=i4Y0\n" + + "-----END PGP MESSAGE-----"; + + public static final String PENC_COMP_LIT = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "hF4Dyqa/GWUy6WsSAQdAuGt49sQwdAHH3jPx11V3wSh7Amur3TbnONiQYJmMo3Qw\n" + + "87yBnZCsaB7evxLBgi6PpF3tiytHM60xlrPeKKPpJhu60vNafRM2OOwqk7AdcZw4\n" + + "0kYBEhiioO2btSuafNrQEjYzAgC7K6l7aPCcQObNp4ofryXu1P5vN+vUZp357hyS\n" + + "6zZqP+0wJQ9yJZMvFTtFeSaSi0oMP2sb\n" + + "=LvRL\n" + + "-----END PGP MESSAGE-----"; + + public static final String OPS_LIT_SIG = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "kA0DAAoWKALh4BJQXl4ByxRiAAAAAABIZWxsbywgV29ybGQhCoh1BAAWCgAnBQJj\n" + + "I3fSCRAoAuHgElBeXhYhBIzd0YicQn/08mDkIygC4eASUF5eAADLOgEA766VyMMv\n" + + "sxfQwQHly3T6ySHSNhYEpoyvdxVqhjBBR+EA/3i6C8lKFPPTh/PvTGbVFOl+eUSV\n" + + "I0w3c+BRY/pO0m4H\n" + + "=tkTV\n" + + "-----END PGP MESSAGE-----"; + + public static void main(String[] args) throws Exception { + // genLIT(); + // genLIT_LIT(); + // genCOMP_LIT(); + // genCOMP(); + // genCOMP_COMP_LIT(); + // genKey(); + // genSIG_LIT(); + // genSENC_LIT(); + genPENC_COMP_LIT(); + // genOPS_LIT_SIG(); + } + + public static void genLIT() throws IOException { + ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out); + PGPLiteralDataGenerator litGen = new PGPLiteralDataGenerator(); + OutputStream litOut = litGen.open(armorOut, PGPLiteralDataGenerator.BINARY, "", PGPLiteralData.NOW, new byte[1 << 9]); + litOut.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); + litOut.close(); + armorOut.close(); + } + + public static void genLIT_LIT() throws IOException { + ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out); + PGPLiteralDataGenerator litGen = new PGPLiteralDataGenerator(); + OutputStream litOut = litGen.open(armorOut, PGPLiteralDataGenerator.BINARY, "", PGPLiteralData.NOW, new byte[1 << 9]); + litOut.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); + litOut.close(); + + litOut = litGen.open(armorOut, PGPLiteralDataGenerator.BINARY, "", PGPLiteralData.NOW, new byte[1 << 9]); + litOut.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); + litOut.close(); + + armorOut.close(); + } + + public static void genCOMP_LIT() throws IOException { + ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out); + PGPCompressedDataGenerator compGen = new PGPCompressedDataGenerator(CompressionAlgorithmTags.ZIP); + OutputStream compOut = compGen.open(armorOut); + PGPLiteralDataGenerator litGen = new PGPLiteralDataGenerator(); + OutputStream litOut = litGen.open(compOut, PGPLiteralDataGenerator.BINARY, "", PGPLiteralData.NOW, new byte[1 << 9]); + litOut.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); + litOut.close(); + compOut.close(); + armorOut.close(); + } + + public static void genCOMP() throws IOException { + ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out); + PGPCompressedDataGenerator compGen = new PGPCompressedDataGenerator(CompressionAlgorithmTags.ZIP); + OutputStream compOut = compGen.open(armorOut); + compOut.close(); + armorOut.close(); + } + + public static void genCOMP_COMP_LIT() throws IOException { + ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out); + + PGPCompressedDataGenerator compGen1 = new PGPCompressedDataGenerator(CompressionAlgorithmTags.ZIP); + OutputStream compOut1 = compGen1.open(armorOut); + + PGPCompressedDataGenerator compGen2 = new PGPCompressedDataGenerator(CompressionAlgorithmTags.BZIP2); + OutputStream compOut2 = compGen2.open(compOut1); + + PGPLiteralDataGenerator litGen = new PGPLiteralDataGenerator(); + OutputStream litOut = litGen.open(compOut2, PGPLiteralDataGenerator.BINARY, "", PGPLiteralDataGenerator.NOW, new byte[1 << 9]); + + litOut.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); + litOut.close(); + compOut2.close(); + compOut1.close(); + armorOut.close(); + } + + public static void genKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + PGPainless.asciiArmor( + PGPainless.generateKeyRing().modernKeyRing("Alice "), + System.out); + } + + public static void genSIG_COMP_LIT() throws PGPException, IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); + ByteArrayOutputStream msgOut = new ByteArrayOutputStream(); + EncryptionStream signer = PGPainless.encryptAndOrSign() + .onOutputStream(msgOut) + .withOptions( + ProducerOptions.sign( + SigningOptions.get() + .addDetachedSignature(SecretKeyRingProtector.unprotectedKeys(), secretKeys) + ).setAsciiArmor(false) + ); + + Streams.pipeAll(new ByteArrayInputStream(PLAINTEXT.getBytes(StandardCharsets.UTF_8)), signer); + signer.close(); + EncryptionResult result = signer.getResult(); + PGPSignature detachedSignature = result.getDetachedSignatures().get(result.getDetachedSignatures().keySet().iterator().next()).iterator().next(); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ArmoredOutputStream armorOut = new ArmoredOutputStream(out); + armorOut.flush(); + detachedSignature.encode(armorOut); + armorOut.write(msgOut.toByteArray()); + armorOut.close(); + + String armored = out.toString(); + // CHECKSTYLE:OFF + System.out.println(armored + .replace("-----BEGIN PGP SIGNATURE-----\n", "-----BEGIN PGP MESSAGE-----\n") + .replace("-----END PGP SIGNATURE-----", "-----END PGP MESSAGE-----")); + // CHECKSTYLE:ON + } + + public static void genSENC_LIT() throws PGPException, IOException { + EncryptionStream enc = PGPainless.encryptAndOrSign() + .onOutputStream(System.out) + .withOptions(ProducerOptions.encrypt(EncryptionOptions.get() + .addMessagePassphrase(Passphrase.fromPassword(PASSPHRASE))) + .overrideCompressionAlgorithm(CompressionAlgorithm.UNCOMPRESSED)); + enc.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); + enc.close(); + } + + public static void genPENC_COMP_LIT() throws IOException, PGPException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); + PGPPublicKeyRing cert = PGPainless.extractCertificate(secretKeys); + EncryptionStream enc = PGPainless.encryptAndOrSign() + .onOutputStream(System.out) + .withOptions(ProducerOptions.encrypt(EncryptionOptions.get() + .addRecipient(cert)) + .overrideCompressionAlgorithm(CompressionAlgorithm.ZLIB)); + + Streams.pipeAll(new ByteArrayInputStream(PLAINTEXT.getBytes(StandardCharsets.UTF_8)), enc); + enc.close(); + } + + public static void genOPS_LIT_SIG() throws PGPException, IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); + + EncryptionStream enc = PGPainless.encryptAndOrSign() + .onOutputStream(System.out) + .withOptions(ProducerOptions.sign(SigningOptions.get() + .addSignature(SecretKeyRingProtector.unprotectedKeys(), secretKeys)) + .overrideCompressionAlgorithm(CompressionAlgorithm.UNCOMPRESSED)); + Streams.pipeAll(new ByteArrayInputStream(PLAINTEXT.getBytes(StandardCharsets.UTF_8)), enc); + enc.close(); + } + + interface Processor { + Tuple process(String armoredMessage, ConsumerOptions options) throws PGPException, IOException; + } + + private static Stream provideMessageProcessors() { + return Stream.of( + Arguments.of(Named.of("read(buf,off,len)", (Processor) OpenPgpMessageInputStreamTest::processReadBuffered)), + Arguments.of(Named.of("read()", (Processor) OpenPgpMessageInputStreamTest::processReadSequential)) + ); + } + + @ParameterizedTest(name = "Process LIT using {0}") + @MethodSource("provideMessageProcessors") + public void testProcessLIT(Processor processor) throws IOException, PGPException { + Tuple result = processor.process(LIT, ConsumerOptions.get()); + String plain = result.getA(); + assertEquals(PLAINTEXT, plain); + + MessageMetadata metadata = result.getB(); + assertNull(metadata.getCompressionAlgorithm()); + assertNull(metadata.getEncryptionAlgorithm()); + assertEquals("", metadata.getFilename()); + JUtils.assertDateEquals(new Date(0L), metadata.getModificationDate()); + assertEquals(StreamEncoding.BINARY, metadata.getLiteralDataEncoding()); + assertTrue(metadata.getVerifiedInlineSignatures().isEmpty()); + assertTrue(metadata.getRejectedInlineSignatures().isEmpty()); + } + + @ParameterizedTest(name = "Process LIT LIT using {0}") + @MethodSource("provideMessageProcessors") + public void testProcessLIT_LIT_fails(Processor processor) { + assertThrows(MalformedOpenPgpMessageException.class, + () -> processor.process(LIT_LIT, ConsumerOptions.get())); + } + + @ParameterizedTest(name = "Process COMP(LIT) using {0}") + @MethodSource("provideMessageProcessors") + public void testProcessCOMP_LIT(Processor processor) + throws PGPException, IOException { + Tuple result = processor.process(COMP_LIT, ConsumerOptions.get()); + String plain = result.getA(); + assertEquals(PLAINTEXT, plain); + MessageMetadata metadata = result.getB(); + assertEquals(CompressionAlgorithm.ZIP, metadata.getCompressionAlgorithm()); + assertTrue(metadata.getVerifiedInlineSignatures().isEmpty()); + assertTrue(metadata.getRejectedInlineSignatures().isEmpty()); + } + + @ParameterizedTest(name = "Process COMP using {0}") + @MethodSource("provideMessageProcessors") + public void testProcessCOMP_fails(Processor processor) { + assertThrows(MalformedOpenPgpMessageException.class, + () -> processor.process(COMP, ConsumerOptions.get())); + } + + @ParameterizedTest(name = "Process COMP(COMP(LIT)) using {0}") + @MethodSource("provideMessageProcessors") + public void testProcessCOMP_COMP_LIT(Processor processor) + throws PGPException, IOException { + Tuple result = processor.process(COMP_COMP_LIT, ConsumerOptions.get()); + String plain = result.getA(); + assertEquals(PLAINTEXT, plain); + MessageMetadata metadata = result.getB(); + assertEquals(CompressionAlgorithm.ZIP, metadata.getCompressionAlgorithm()); + Iterator compressionAlgorithms = metadata.getCompressionAlgorithms(); + assertEquals(CompressionAlgorithm.ZIP, compressionAlgorithms.next()); + assertEquals(CompressionAlgorithm.BZIP2, compressionAlgorithms.next()); + assertFalse(compressionAlgorithms.hasNext()); + assertNull(metadata.getEncryptionAlgorithm()); + assertTrue(metadata.getVerifiedInlineSignatures().isEmpty()); + assertTrue(metadata.getRejectedInlineSignatures().isEmpty()); + } + + @ParameterizedTest(name = "Process SIG COMP(LIT) using {0}") + @MethodSource("provideMessageProcessors") + public void testProcessSIG_COMP_LIT(Processor processor) + throws PGPException, IOException { + PGPPublicKeyRing cert = PGPainless.extractCertificate( + PGPainless.readKeyRing().secretKeyRing(KEY)); + + Tuple result = processor.process(SIG_COMP_LIT, ConsumerOptions.get() + .addVerificationCert(cert)); + String plain = result.getA(); + assertEquals(PLAINTEXT, plain); + MessageMetadata metadata = result.getB(); + assertEquals(CompressionAlgorithm.ZIP, metadata.getCompressionAlgorithm()); + assertNull(metadata.getEncryptionAlgorithm()); + assertFalse(metadata.getVerifiedInlineSignatures().isEmpty()); + assertTrue(metadata.getRejectedInlineSignatures().isEmpty()); + } + + @ParameterizedTest(name = "Process SENC(LIT) using {0}") + @MethodSource("provideMessageProcessors") + public void testProcessSENC_LIT(Processor processor) + throws PGPException, IOException { + Tuple result = processor.process(SENC_LIT, ConsumerOptions.get() + .addMessagePassphrase(Passphrase.fromPassword(PASSPHRASE))); + String plain = result.getA(); + assertEquals(PLAINTEXT, plain); + MessageMetadata metadata = result.getB(); + assertNull(metadata.getCompressionAlgorithm()); + assertEquals(SymmetricKeyAlgorithm.AES_256, metadata.getEncryptionAlgorithm()); + assertTrue(metadata.getVerifiedInlineSignatures().isEmpty()); + assertTrue(metadata.getRejectedInlineSignatures().isEmpty()); + } + + @ParameterizedTest(name = "Process PENC(COMP(LIT)) using {0}") + @MethodSource("provideMessageProcessors") + public void testProcessPENC_COMP_LIT(Processor processor) + throws IOException, PGPException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); + Tuple result = processor.process(PENC_COMP_LIT, ConsumerOptions.get() + .addDecryptionKey(secretKeys)); + String plain = result.getA(); + assertEquals(PLAINTEXT, plain); + MessageMetadata metadata = result.getB(); + assertEquals(CompressionAlgorithm.ZLIB, metadata.getCompressionAlgorithm()); + assertEquals(SymmetricKeyAlgorithm.AES_256, metadata.getEncryptionAlgorithm()); + assertTrue(metadata.getVerifiedInlineSignatures().isEmpty()); + assertTrue(metadata.getRejectedInlineSignatures().isEmpty()); + } + + @ParameterizedTest(name = "Process OPS LIT SIG using {0}") + @MethodSource("provideMessageProcessors") + public void testProcessOPS_LIT_SIG(Processor processor) + throws IOException, PGPException { + PGPPublicKeyRing cert = PGPainless.extractCertificate(PGPainless.readKeyRing().secretKeyRing(KEY)); + Tuple result = processor.process(OPS_LIT_SIG, ConsumerOptions.get() + .addVerificationCert(cert)); + String plain = result.getA(); + assertEquals(PLAINTEXT, plain); + MessageMetadata metadata = result.getB(); + assertNull(metadata.getEncryptionAlgorithm()); + assertNull(metadata.getCompressionAlgorithm()); + assertFalse(metadata.getVerifiedInlineSignatures().isEmpty()); + assertTrue(metadata.getRejectedInlineSignatures().isEmpty()); + } + + String BOB_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Comment: Bob's OpenPGP Transferable Secret Key\n" + + "\n" + + "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\n" + + "=miES\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + + @ParameterizedTest(name = "Process PENC(OPS OPS LIT SIG SIG) using {0}") + @MethodSource("provideMessageProcessors") + public void testProcessPENC_OPS_OPS_LIT_SIG_SIG(Processor processor) throws IOException, PGPException { + String MSG = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "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\n" + + "=mdYs\n" + + "-----END PGP MESSAGE-----\n"; + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(BOB_KEY); + PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKeys); + + Tuple result = processor.process(MSG, ConsumerOptions.get() + .addVerificationCert(certificate) + .addDecryptionKey(secretKeys)); + String plain = result.getA(); + assertEquals("encrypt ∘ sign ∘ sign", plain); + MessageMetadata metadata = result.getB(); + assertEquals(SymmetricKeyAlgorithm.AES_256, metadata.getEncryptionAlgorithm()); + assertNull(metadata.getCompressionAlgorithm()); + assertFalse(metadata.getVerifiedInlineSignatures().isEmpty()); + assertTrue(metadata.getRejectedInlineSignatures().isEmpty()); + } + + @ParameterizedTest(name = "Process PENC(OPS OPS OPS LIT SIG SIG SIG) using {0}") + @MethodSource("provideMessageProcessors") + public void testProcessOPS_OPS_OPS_LIT_SIG_SIG_SIG(Processor processor) throws IOException, PGPException { + String MSG = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "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\n" + + "=JP9T\n" + + "-----END PGP MESSAGE-----"; + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(BOB_KEY); + PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKeys); + + Tuple result = processor.process(MSG, ConsumerOptions.get() + .addVerificationCert(certificate) + .addDecryptionKey(secretKeys)); + String plain = result.getA(); + assertEquals("encrypt ∘ sign ∘ sign ∘ sign", plain); + MessageMetadata metadata = result.getB(); + assertEquals(SymmetricKeyAlgorithm.AES_256, metadata.getEncryptionAlgorithm()); + assertNull(metadata.getCompressionAlgorithm()); + assertFalse(metadata.getVerifiedInlineSignatures().isEmpty()); + assertTrue(metadata.getRejectedInlineSignatures().isEmpty()); + } + + @Test + public void readAfterCloseTest() throws PGPException, IOException { + OpenPgpMessageInputStream pgpIn = get(SENC_LIT, ConsumerOptions.get() + .addMessagePassphrase(Passphrase.fromPassword(PASSPHRASE))); + Streams.drain(pgpIn); // read all + + byte[] buf = new byte[1024]; + assertEquals(-1, pgpIn.read(buf)); + assertEquals(-1, pgpIn.read()); + assertEquals(-1, pgpIn.read(buf)); + assertEquals(-1, pgpIn.read()); + + pgpIn.close(); + pgpIn.getMetadata(); + } + + private static Tuple processReadBuffered(String armoredMessage, ConsumerOptions options) + throws PGPException, IOException { + OpenPgpMessageInputStream in = get(armoredMessage, options); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Streams.pipeAll(in, out); + in.close(); + MessageMetadata metadata = in.getMetadata(); + return new Tuple<>(out.toString(), metadata); + } + + private static Tuple processReadSequential(String armoredMessage, ConsumerOptions options) + throws PGPException, IOException { + OpenPgpMessageInputStream in = get(armoredMessage, options); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + int r; + while ((r = in.read()) != -1) { + out.write(r); + } + + in.close(); + MessageMetadata metadata = in.getMetadata(); + return new Tuple<>(out.toString(), metadata); + } + + private static OpenPgpMessageInputStream get(String armored, ConsumerOptions options) + throws IOException, PGPException { + ByteArrayInputStream bytesIn = new ByteArrayInputStream(armored.getBytes(StandardCharsets.UTF_8)); + ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); + OpenPgpMessageInputStream pgpIn = OpenPgpMessageInputStream.create(armorIn, options); + return pgpIn; + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PostponeDecryptionUsingKeyWithMissingPassphraseTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PostponeDecryptionUsingKeyWithMissingPassphraseTest.java index 4aad6c9e..8489da9a 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PostponeDecryptionUsingKeyWithMissingPassphraseTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PostponeDecryptionUsingKeyWithMissingPassphraseTest.java @@ -11,7 +11,6 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; -import javax.annotation.Nullable; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPSecretKeyRing; @@ -120,7 +119,6 @@ public class PostponeDecryptionUsingKeyWithMissingPassphraseTest { @Test public void missingPassphraseFirst() throws PGPException, IOException { SecretKeyRingProtector protector1 = new CachingSecretKeyRingProtector(new SecretKeyPassphraseProvider() { - @Nullable @Override public Passphrase getPassphraseFor(Long keyId) { fail("Although the first PKESK is for k1, we should have skipped it and tried k2 first, which has passphrase available."); @@ -132,7 +130,7 @@ public class PostponeDecryptionUsingKeyWithMissingPassphraseTest { return false; } }); - SecretKeyRingProtector protector2 = SecretKeyRingProtector.unlockAllKeysWith(p2, k2); + SecretKeyRingProtector protector2 = SecretKeyRingProtector.unlockEachKeyWith(p2, k2); DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() .onInputStream(new ByteArrayInputStream(ENCRYPTED_FOR_K1_K2.getBytes(StandardCharsets.UTF_8))) @@ -149,9 +147,8 @@ public class PostponeDecryptionUsingKeyWithMissingPassphraseTest { @Test public void missingPassphraseSecond() throws PGPException, IOException { - SecretKeyRingProtector protector1 = SecretKeyRingProtector.unlockAllKeysWith(p1, k1); + SecretKeyRingProtector protector1 = SecretKeyRingProtector.unlockEachKeyWith(p1, k1); SecretKeyRingProtector protector2 = new CachingSecretKeyRingProtector(new SecretKeyPassphraseProvider() { - @Nullable @Override public Passphrase getPassphraseFor(Long keyId) { fail("This callback should not get called, since the first PKESK is for k1, which has a passphrase available."); @@ -180,7 +177,6 @@ public class PostponeDecryptionUsingKeyWithMissingPassphraseTest { @Test public void messagePassphraseFirst() throws PGPException, IOException { SecretKeyPassphraseProvider provider = new SecretKeyPassphraseProvider() { - @Nullable @Override public Passphrase getPassphraseFor(Long keyId) { fail("Since we provide a decryption passphrase, we should not try to decrypt any key."); @@ -197,7 +193,7 @@ public class PostponeDecryptionUsingKeyWithMissingPassphraseTest { DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() .onInputStream(new ByteArrayInputStream(ENCRYPTED_FOR_K2_PASS_K1.getBytes(StandardCharsets.UTF_8))) .withOptions(new ConsumerOptions() - .addDecryptionPassphrase(PASSPHRASE) + .addMessagePassphrase(PASSPHRASE) .addDecryptionKey(k1, protector) .addDecryptionKey(k2, protector)); diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PreventDecryptionUsingNonEncryptionKeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PreventDecryptionUsingNonEncryptionKeyTest.java new file mode 100644 index 00000000..f06f0233 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PreventDecryptionUsingNonEncryptionKeyTest.java @@ -0,0 +1,220 @@ +// SPDX-FileCopyrightText: 2021 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.exception.MissingDecryptionMethodException; +import org.pgpainless.key.SubkeyIdentifier; + +public class PreventDecryptionUsingNonEncryptionKeyTest { + + private static final String ENCRYPTION_CAPABLE_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 1E9C 0357 913E 797A E3EF 5D15 E683 CE77 5D98 8147\n" + + "Comment: Alice \n" + + "\n" + + "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\n" + + "=JH2v\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + + // same key, but with crippling self-signature with flags [C,S] + private static final String ENCRYPTION_INCAPABLE_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 1E9C 0357 913E 797A E3EF 5D15 E683 CE77 5D98 8147\n" + + "Comment: Alice \n" + + "\n" + + "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\n" + + "sqSmGpGc\n" + + "=3NqQ\n" + + "-----END PGP PRIVATE KEY BLOCK-----\n"; + + private static final String MSG = "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "hQIMA+aDznddmIFHAQ//X9ML2+fboZEJZpw/p2WD7k9SREZ24KdJtdUif98pEoFw\n" + + "yQB5CC2s2gBENJ+2N7UV1esU/oW7Bs+RSvNKl4eIyrH/Tz3ijVd/mcGYcKOxejW8\n" + + "4ES5BWdRj0+9/CV3AGwAKb3g3D39tbiWqh0CH+2Ayq9O8MQo4/zE+fKSjFdfPD/y\n" + + "oLtrv9Fkh6stg+j+1QRgWG/2NIeA7/8JJlyzHvcuq0jpBUhsrVRgm0vjpyZnPl/s\n" + + "tz3wjWBcQD1RQXFTSpgsnbB8e0FkzPGWZ5QtxKHOo8clrTQg9LUtj/S3R0iLN6sI\n" + + "CtiTojpMRSbTOCjdFCcYR/scU+eyqDcZ096EgfBQitj4ALUYOFkILBS9M+Lh3xIz\n" + + "rv3+z0kmRHyKn8kcmTEoqyqkZfuEMOY6gK+hD9garBJ/91tN8ul4uHax7CMwqN8/\n" + + "yKWoATEU7ZHMe7jF6jS5pK8ET8IP2yVfZOhGZW8mcMrUMiF7LG1HepK5p7UMkmbX\n" + + "GHkU3vEU429PL4NXDnuLufObqZBg5zSdIzo/LXtvLKXaHUv0am1JW2wQD8hAUJAt\n" + + "Uz+NFncS+a8bbsu1wNRjAr6Rg/5VHEk/5h+zuZP8UkCQp5NID26oVvnfDkTXAZ1+\n" + + "egh7coZ4a2IqEVBDWnkAXssxGBxVwGDr14oNC1SaABWzqPxaY0yVrpZcr30ghqPS\n" + + "PwHcN1NCaMtJDH4ThxH5L6zHboQeX2R6x9vpWLu9FDFqEDilxHtw+7Lax4yralGC\n" + + "q3K54WgyYD2Q2tzMXOTEwg==\n" + + "=+Ttf\n" + + "-----END PGP MESSAGE-----"; + + @Test + public void baseCase() throws PGPException, IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(ENCRYPTION_CAPABLE_KEY); + + ByteArrayInputStream msgIn = new ByteArrayInputStream(MSG.getBytes(StandardCharsets.UTF_8)); + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(msgIn) + .withOptions(new ConsumerOptions().addDecryptionKey(secretKeys)); + + Streams.drain(decryptionStream); + decryptionStream.close(); + MessageMetadata metadata = decryptionStream.getMetadata(); + + assertEquals(new SubkeyIdentifier(secretKeys, secretKeys.getPublicKey().getKeyID()), metadata.getDecryptionKey()); + } + + @Test + public void canDecryptMessageDespiteMissingKeyFlag() throws IOException, PGPException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(ENCRYPTION_INCAPABLE_KEY); + + ByteArrayInputStream msgIn = new ByteArrayInputStream(MSG.getBytes(StandardCharsets.UTF_8)); + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(msgIn) + .withOptions(new ConsumerOptions().addDecryptionKey(secretKeys)); + + Streams.drain(decryptionStream); + decryptionStream.close(); + MessageMetadata metadata = decryptionStream.getMetadata(); + + assertEquals(new SubkeyIdentifier(secretKeys, secretKeys.getPublicKey().getKeyID()), metadata.getDecryptionKey()); + } + + @Test + @Disabled + public void nonEncryptionKeyCannotDecrypt() throws IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(ENCRYPTION_INCAPABLE_KEY); + + ByteArrayInputStream msgIn = new ByteArrayInputStream(MSG.getBytes(StandardCharsets.UTF_8)); + + assertThrows(MissingDecryptionMethodException.class, () -> + PGPainless.decryptAndOrVerify() + .onInputStream(msgIn) + .withOptions(new ConsumerOptions().addDecryptionKey(secretKeys))); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/RecursionDepthTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/RecursionDepthTest.java index 5bc1ae61..52c5a906 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/RecursionDepthTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/RecursionDepthTest.java @@ -11,13 +11,13 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; -import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.util.io.Streams; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; import org.pgpainless.PGPainless; -import org.pgpainless.implementation.ImplementationFactory; +import org.pgpainless.exception.MalformedOpenPgpMessageException; +import org.pgpainless.util.TestAllImplementations; public class RecursionDepthTest { @@ -26,10 +26,10 @@ public class RecursionDepthTest { * * @see Sequoia-PGP Test Suite */ - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void decryptionAbortsWhenMaximumRecursionDepthReachedTest(ImplementationFactory implementationFactory) throws IOException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void decryptionAbortsWhenMaximumRecursionDepthReachedTest() + throws IOException { String key = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + "Comment: Bob's OpenPGP Transferable Secret Key\n" + "\n" + @@ -143,7 +143,7 @@ public class RecursionDepthTest { "-----END PGP ARMORED FILE-----\n"; - assertThrows(PGPException.class, () -> { + assertThrows(MalformedOpenPgpMessageException.class, () -> { DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() .onInputStream(new ByteArrayInputStream(msg.getBytes(StandardCharsets.UTF_8))) .withOptions(new ConsumerOptions().addDecryptionKey(secretKey)); diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/RejectWeakSymmetricAlgorithmDuringDecryption.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/RejectWeakSymmetricAlgorithmDuringDecryptionTest.java similarity index 99% rename from pgpainless-core/src/test/java/org/pgpainless/decryption_verification/RejectWeakSymmetricAlgorithmDuringDecryption.java rename to pgpainless-core/src/test/java/org/pgpainless/decryption_verification/RejectWeakSymmetricAlgorithmDuringDecryptionTest.java index 53c690ed..1201ef69 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/RejectWeakSymmetricAlgorithmDuringDecryption.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/RejectWeakSymmetricAlgorithmDuringDecryptionTest.java @@ -22,7 +22,7 @@ import org.pgpainless.exception.UnacceptableAlgorithmException; * Test PGPainless' default symmetric key algorithm policy for decryption of messages. * The default decryption policy rejects messages encrypted with IDEA and TripleDES, as well as unencrypted messages. */ -public class RejectWeakSymmetricAlgorithmDuringDecryption { +public class RejectWeakSymmetricAlgorithmDuringDecryptionTest { private static final String key = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + " Comment: Bob's OpenPGP Transferable Secret Key\n" + diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/SignedMessageVerificationWithoutCertIsStillSigned.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/SignedMessageVerificationWithoutCertIsStillSignedTest.java similarity index 79% rename from pgpainless-core/src/test/java/org/pgpainless/decryption_verification/SignedMessageVerificationWithoutCertIsStillSigned.java rename to pgpainless-core/src/test/java/org/pgpainless/decryption_verification/SignedMessageVerificationWithoutCertIsStillSignedTest.java index 0e979ead..9f85b241 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/SignedMessageVerificationWithoutCertIsStillSigned.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/SignedMessageVerificationWithoutCertIsStillSignedTest.java @@ -17,7 +17,7 @@ import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; -public class SignedMessageVerificationWithoutCertIsStillSigned { +public class SignedMessageVerificationWithoutCertIsStillSignedTest { private static final String message = "-----BEGIN PGP MESSAGE-----\n" + "\n" + @@ -39,9 +39,10 @@ public class SignedMessageVerificationWithoutCertIsStillSigned { Streams.pipeAll(verificationStream, out); verificationStream.close(); - OpenPgpMetadata metadata = verificationStream.getResult(); + MessageMetadata metadata = verificationStream.getMetadata(); - assertTrue(metadata.isSigned(), "Message is signed, even though we miss the verification cert."); - assertFalse(metadata.isVerified(), "Message is not verified because we lack the verification cert."); + assertFalse(metadata.isUsingCleartextSignatureFramework()); + assertTrue(metadata.hasRejectedSignatures(), "Message is signed, even though we miss the verification cert."); + assertFalse(metadata.isVerifiedSigned(), "Message is not verified because we lack the verification cert."); } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TestDecryptionOfMessageWithoutESKUsingSessionKey.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TestDecryptionOfMessageWithoutESKUsingSessionKey.java new file mode 100644 index 00000000..b28af822 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TestDecryptionOfMessageWithoutESKUsingSessionKey.java @@ -0,0 +1,69 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPSessionKey; +import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.util.SessionKey; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class TestDecryptionOfMessageWithoutESKUsingSessionKey { + + private static final String encryptedMessageWithSKESK = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "jA0ECQMCc7jNEadAMZJg0j8BNtJwO2PLoRdG+VynivV7XpHp2Nw/S489vksUKct6\n" + + "7CYTFpVTzB4IcJwmUGMmre/N1KMTznEBzy3Txa1QVBc=\n" + + "=3M8l\n" + + "-----END PGP MESSAGE-----"; + + private static final String encryptedMessageWithoutESK = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "0j8BNtJwO2PLoRdG+VynivV7XpHp2Nw/S489vksUKct67CYTFpVTzB4IcJwmUGMm\n" + + "re/N1KMTznEBzy3Txa1QVBc=\n" + + "=t+pk\n" + + "-----END PGP MESSAGE-----"; + + private static final SessionKey sessionKey = new SessionKey( + PGPSessionKey.fromAsciiRepresentation("9:26be99bc478520fbc8ab8fb84991dace4b82cfb9b00f7d05c051d69b8cea8a7f")); + + @Test + public void decryptMessageWithSKESK() throws PGPException, IOException { + ByteArrayInputStream in = new ByteArrayInputStream(encryptedMessageWithSKESK.getBytes(StandardCharsets.UTF_8)); + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(in) + .withOptions(ConsumerOptions.get() + .setSessionKey(sessionKey)); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Streams.pipeAll(decryptionStream, out); + decryptionStream.close(); + assertEquals("Hello, World!\n", out.toString()); + } + + @Test + public void decryptMessageWithoutSKESK() throws PGPException, IOException { + ByteArrayInputStream in = new ByteArrayInputStream(encryptedMessageWithoutESK.getBytes(StandardCharsets.UTF_8)); + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(in) + .withOptions(ConsumerOptions.get() + .setSessionKey(sessionKey)); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Streams.pipeAll(decryptionStream, out); + decryptionStream.close(); + assertEquals("Hello, World!\n", out.toString()); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TryDecryptWithUnavailableGnuDummyKeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TryDecryptWithUnavailableGnuDummyKeyTest.java new file mode 100644 index 00000000..640025b1 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TryDecryptWithUnavailableGnuDummyKeyTest.java @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.encryption_signing.EncryptionOptions; +import org.pgpainless.encryption_signing.EncryptionStream; +import org.pgpainless.encryption_signing.ProducerOptions; +import org.gnupg.GnuPGDummyKeyUtil; +import org.pgpainless.exception.MissingDecryptionMethodException; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class TryDecryptWithUnavailableGnuDummyKeyTest { + + @Test + public void testAttemptToDecryptWithRemovedPrivateKeysThrows() + throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() + .modernKeyRing("Hardy Hardware "); + PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKeys); + + ByteArrayOutputStream ciphertextOut = new ByteArrayOutputStream(); + EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() + .onOutputStream(ciphertextOut) + .withOptions( + ProducerOptions.encrypt(EncryptionOptions.get().addRecipient(certificate))); + ByteArrayInputStream plaintextIn = new ByteArrayInputStream("Hello, World!\n".getBytes()); + Streams.pipeAll(plaintextIn, encryptionStream); + encryptionStream.close(); + + PGPSecretKeyRing removedKeys = GnuPGDummyKeyUtil.modify(secretKeys) + .removePrivateKeys(GnuPGDummyKeyUtil.KeyFilter.any()); + + ByteArrayInputStream ciphertextIn = new ByteArrayInputStream(ciphertextOut.toByteArray()); + assertThrows(MissingDecryptionMethodException.class, () -> PGPainless.decryptAndOrVerify() + .onInputStream(ciphertextIn) + .withOptions(ConsumerOptions.get().addDecryptionKey(removedKeys))); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/UnsupportedPacketVersionsTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/UnsupportedPacketVersionsTest.java new file mode 100644 index 00000000..f8ce9e29 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/UnsupportedPacketVersionsTest.java @@ -0,0 +1,407 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.Charset; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; + +public class UnsupportedPacketVersionsTest { + + private static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Comment: Bob's OpenPGP Transferable Secret Key\n" + + "\n" + + "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\n" + + "=miES\n" + + "-----END PGP PRIVATE KEY BLOCK-----\n"; + + private static final String PKESK3_PKESK23_SEIP = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "wcDMA3wvqk35PDeyAQv/YP+fDWtifT7KSk+tWrgbyvsYCt5Wh0IPESTZuiptwvto\n" + + "CGbOfwuPbqqzzlFqSvX3UiJwhxjSSB3a1EBIOsbhc4grip/wm+fB50S/nTJxkJ14\n" + + "qid40D7HOcIvuz1iQr1QoMNB0oT3nCwMec8mPUX2yOzx1eqr62SZUTCr6FdAmdYI\n" + + "1u4EAeEFhRO0rcPRrpMZqwkXtUfx+pu7OzBS0qmOlfkQ50kbETDXBik4iXi30AGl\n" + + "Ifo792oRo6DFK7ENquTNRqFPfezjrGZfkJrPWulWh28GogWTpOBwfXG8X262QoIp\n" + + "VwZygi7wfj1jh2sXPvWgHjsjjTt7HPAiLI1f6IUl8WCQfPuQkFwCwPv63/rve59v\n" + + "sBaeCEykAxdzMbP1oYSBBtONSAPYW9fsUsJSpuuLvxH252+luk09uQXWd6z4aCDm\n" + + "EXiolhbkzL3mXCpVP6nMjRkm2ERE1yAWgXGT9JON0gcCb3eVqw6wzOYu+Vwq70ND\n" + + "vKYlTMY+9RUx7wLn51UgwUoXQUFBQUFBQUEJYWFhYWFhYWFhYWFhYWFhYWFhYWFh\n" + + "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYdJMAQZo\n" + + "tUFmYcTQrd5IFriHbF8h/Ov/xKlkW1QOPrZ+ziMQRbwyY4pVwNbZjCNVCHg5QDO4\n" + + "wjF686DfZt83NvVYbJ7QNuENoI4YcFj8nw==\n" + + "=VS1M\n" + + "-----END PGP MESSAGE-----"; + + private static final String PKESK23_PKESK3_SEIP = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "wUoXQUFBQUFBQUEJYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFh\n" + + "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYcHAzAN8L6pN+Tw3sgEL/2D/nw1r\n" + + "Yn0+ykpPrVq4G8r7GAreVodCDxEk2boqbcL7aAhmzn8Lj26qs85Rakr191IicIcY\n" + + "0kgd2tRASDrG4XOIK4qf8JvnwedEv50ycZCdeKoneNA+xznCL7s9YkK9UKDDQdKE\n" + + "95wsDHnPJj1F9sjs8dXqq+tkmVEwq+hXQJnWCNbuBAHhBYUTtK3D0a6TGasJF7VH\n" + + "8fqbuzswUtKpjpX5EOdJGxEw1wYpOIl4t9ABpSH6O/dqEaOgxSuxDarkzUahT33s\n" + + "46xmX5Caz1rpVodvBqIFk6TgcH1xvF9utkKCKVcGcoIu8H49Y4drFz71oB47I407\n" + + "exzwIiyNX+iFJfFgkHz7kJBcAsD7+t/673ufb7AWnghMpAMXczGz9aGEgQbTjUgD\n" + + "2FvX7FLCUqbri78R9udvpbpNPbkF1nes+Ggg5hF4qJYW5My95lwqVT+pzI0ZJthE\n" + + "RNcgFoFxk/STjdIHAm93lasOsMzmLvlcKu9DQ7ymJUzGPvUVMe8C5+dVINJMAQZo\n" + + "tUFmYcTQrd5IFriHbF8h/Ov/xKlkW1QOPrZ+ziMQRbwyY4pVwNbZjCNVCHg5QDO4\n" + + "wjF686DfZt83NvVYbJ7QNuENoI4YcFj8nw==\n" + + "=EhNy\n" + + "-----END PGP MESSAGE-----\n"; + + private static final String PKESK3_SKESK23_SEIP = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "wcDMA3wvqk35PDeyAQv/YP+fDWtifT7KSk+tWrgbyvsYCt5Wh0IPESTZuiptwvto\n" + + "CGbOfwuPbqqzzlFqSvX3UiJwhxjSSB3a1EBIOsbhc4grip/wm+fB50S/nTJxkJ14\n" + + "qid40D7HOcIvuz1iQr1QoMNB0oT3nCwMec8mPUX2yOzx1eqr62SZUTCr6FdAmdYI\n" + + "1u4EAeEFhRO0rcPRrpMZqwkXtUfx+pu7OzBS0qmOlfkQ50kbETDXBik4iXi30AGl\n" + + "Ifo792oRo6DFK7ENquTNRqFPfezjrGZfkJrPWulWh28GogWTpOBwfXG8X262QoIp\n" + + "VwZygi7wfj1jh2sXPvWgHjsjjTt7HPAiLI1f6IUl8WCQfPuQkFwCwPv63/rve59v\n" + + "sBaeCEykAxdzMbP1oYSBBtONSAPYW9fsUsJSpuuLvxH252+luk09uQXWd6z4aCDm\n" + + "EXiolhbkzL3mXCpVP6nMjRkm2ERE1yAWgXGT9JON0gcCb3eVqw6wzOYu+Vwq70ND\n" + + "vKYlTMY+9RUx7wLn51Ugw00XCQMINQp7MFzAc6T/YWFhYWFhYWFhYWFhYWFhYWFh\n" + + "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYdJM\n" + + "AQZotUFmYcTQrd5IFriHbF8h/Ov/xKlkW1QOPrZ+ziMQRbwyY4pVwNbZjCNVCHg5\n" + + "QDO4wjF686DfZt83NvVYbJ7QNuENoI4YcFj8nw==\n" + + "=pvWj\n" + + "-----END PGP MESSAGE-----\n"; + + private static final String SKESK23_PKESK3_SEIP = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "w00XCQMINQp7MFzAc6T/YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFh\n" + + "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYcHAzAN8L6pN+Tw3sgEL/2D/\n" + + "nw1rYn0+ykpPrVq4G8r7GAreVodCDxEk2boqbcL7aAhmzn8Lj26qs85Rakr191Ii\n" + + "cIcY0kgd2tRASDrG4XOIK4qf8JvnwedEv50ycZCdeKoneNA+xznCL7s9YkK9UKDD\n" + + "QdKE95wsDHnPJj1F9sjs8dXqq+tkmVEwq+hXQJnWCNbuBAHhBYUTtK3D0a6TGasJ\n" + + "F7VH8fqbuzswUtKpjpX5EOdJGxEw1wYpOIl4t9ABpSH6O/dqEaOgxSuxDarkzUah\n" + + "T33s46xmX5Caz1rpVodvBqIFk6TgcH1xvF9utkKCKVcGcoIu8H49Y4drFz71oB47\n" + + "I407exzwIiyNX+iFJfFgkHz7kJBcAsD7+t/673ufb7AWnghMpAMXczGz9aGEgQbT\n" + + "jUgD2FvX7FLCUqbri78R9udvpbpNPbkF1nes+Ggg5hF4qJYW5My95lwqVT+pzI0Z\n" + + "JthERNcgFoFxk/STjdIHAm93lasOsMzmLvlcKu9DQ7ymJUzGPvUVMe8C5+dVINJM\n" + + "AQZotUFmYcTQrd5IFriHbF8h/Ov/xKlkW1QOPrZ+ziMQRbwyY4pVwNbZjCNVCHg5\n" + + "QDO4wjF686DfZt83NvVYbJ7QNuENoI4YcFj8nw==\n" + + "=STOd\n" + + "-----END PGP MESSAGE-----\n"; + + private static final String PKESK3_SKESK4wS2K23_SEIP = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "wcDMA3wvqk35PDeyAQv/YP+fDWtifT7KSk+tWrgbyvsYCt5Wh0IPESTZuiptwvto\n" + + "CGbOfwuPbqqzzlFqSvX3UiJwhxjSSB3a1EBIOsbhc4grip/wm+fB50S/nTJxkJ14\n" + + "qid40D7HOcIvuz1iQr1QoMNB0oT3nCwMec8mPUX2yOzx1eqr62SZUTCr6FdAmdYI\n" + + "1u4EAeEFhRO0rcPRrpMZqwkXtUfx+pu7OzBS0qmOlfkQ50kbETDXBik4iXi30AGl\n" + + "Ifo792oRo6DFK7ENquTNRqFPfezjrGZfkJrPWulWh28GogWTpOBwfXG8X262QoIp\n" + + "VwZygi7wfj1jh2sXPvWgHjsjjTt7HPAiLI1f6IUl8WCQfPuQkFwCwPv63/rve59v\n" + + "sBaeCEykAxdzMbP1oYSBBtONSAPYW9fsUsJSpuuLvxH252+luk09uQXWd6z4aCDm\n" + + "EXiolhbkzL3mXCpVP6nMjRkm2ERE1yAWgXGT9JON0gcCb3eVqw6wzOYu+Vwq70ND\n" + + "vKYlTMY+9RUx7wLn51Ugw1AECRcIYWFhYWFhYWFBQUFBYWFhYWFhYWFhYWFhYWFh\n" + + "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFh\n" + + "YdJMAQZotUFmYcTQrd5IFriHbF8h/Ov/xKlkW1QOPrZ+ziMQRbwyY4pVwNbZjCNV\n" + + "CHg5QDO4wjF686DfZt83NvVYbJ7QNuENoI4YcFj8nw==\n" + + "=/uxY\n" + + "-----END PGP MESSAGE-----\n"; + + private static final String SKESK4wS2K23_PKESK3_SEIP = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "w1AECRcIYWFhYWFhYWFBQUFBYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFh\n" + + "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYcHAzAN8L6pN+Tw3sgEL\n" + + "/2D/nw1rYn0+ykpPrVq4G8r7GAreVodCDxEk2boqbcL7aAhmzn8Lj26qs85Rakr1\n" + + "91IicIcY0kgd2tRASDrG4XOIK4qf8JvnwedEv50ycZCdeKoneNA+xznCL7s9YkK9\n" + + "UKDDQdKE95wsDHnPJj1F9sjs8dXqq+tkmVEwq+hXQJnWCNbuBAHhBYUTtK3D0a6T\n" + + "GasJF7VH8fqbuzswUtKpjpX5EOdJGxEw1wYpOIl4t9ABpSH6O/dqEaOgxSuxDark\n" + + "zUahT33s46xmX5Caz1rpVodvBqIFk6TgcH1xvF9utkKCKVcGcoIu8H49Y4drFz71\n" + + "oB47I407exzwIiyNX+iFJfFgkHz7kJBcAsD7+t/673ufb7AWnghMpAMXczGz9aGE\n" + + "gQbTjUgD2FvX7FLCUqbri78R9udvpbpNPbkF1nes+Ggg5hF4qJYW5My95lwqVT+p\n" + + "zI0ZJthERNcgFoFxk/STjdIHAm93lasOsMzmLvlcKu9DQ7ymJUzGPvUVMe8C5+dV\n" + + "INJMAQZotUFmYcTQrd5IFriHbF8h/Ov/xKlkW1QOPrZ+ziMQRbwyY4pVwNbZjCNV\n" + + "CHg5QDO4wjF686DfZt83NvVYbJ7QNuENoI4YcFj8nw==\n" + + "=cIwV\n" + + "-----END PGP MESSAGE-----\n"; + + private static final String PKESK3_SEIP_OPS3_LIT_SIG4 = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "wcDMA3wvqk35PDeyAQv+IhkCMhbdRcMnIPZNPGU6OK1Jk5xuRdIEIBsvv7b8jmAr\n" + + "9IwjfnV/RDMtH+xR/T9K7qJGGFYnhLY5w0CmYHQcDKpcBqk0Dw6l/eKCNhgRXKAk\n" + + "gfaKL1Utt1Pw0nz0mOwHyPEN/pGc0xlVhsjVkRvIOsKcpfuc1EpSZMFgDcBQDhe/\n" + + "jAsR/MvRugkW8xLpyQyfeGLJUOEYVrkpam3rLKB1KywAgBmpr9WDwfYITW/VE9k5\n" + + "cKIOPMDJFU+u9lzBx6nSS4JRBuCO2mhR4gjcRaGPWiiz+0qZfS+AXYV/MAU5OwK/\n" + + "6nhX97zwS4r1Avztjh4taBhLVsY4pw6PuLtACJNwPrev63Yc+a4hJJ4pm1AnHV58\n" + + "Y1pZKQL8vA61+/tbhFQ18vbJ8E1NOka/euFLQu9Mg58jhpcscqMouyr3JFMwgH2Y\n" + + "eFuRJncJAKotXxlfnF37qz5LG3bamACXWZSObjp9d4quIAoCDUteZlDWQ1xq5R4y\n" + + "QYXtk9ZuHsHsmY9A0CiI0sGYAUBtLubJ5qhLLr/GqKAmy8jTSA3MjgtrB55NWj9J\n" + + "bjgFNsd1BNGklgnwhtmApHJWY8skAAQkJj0rXj/aOMc734ypiEWDiU1quRbEeRLR\n" + + "kDvBNUXx2j2rVF+MmQS/sm5Yk/op+4lH/Wounsci3qWH76GaNZoIlvNE3mdFoVTe\n" + + "cRh4W2Em8uAUH4bKwazltRJUhZmXvuGfUnQCmolJTpyPl4DaQQgzdBXLTRcPxwdU\n" + + "30e7HnxZWESKx1LnGxp3Oan1k1lXyHwvnEk26EXhve+dhsQ6YsKgvtSLNFqGsfKe\n" + + "MVOq37cpOGFQsYStWHZd0tcHjIjWmAeZ8kH+ZzR9tgYKxxjimsxafLS/lo415SkC\n" + + "LnOCz6hywI7CufSUcXUlHGJuobZ5HDJcygsQhQmNVLDmKh4xUJDZrORS0ciMy2kc\n" + + "XAnxCDYbltVQktc/F/Gl1lTx0UNmV5d9G0utVmxxGbXna3BLkB+6qMuux/ngC+cI\n" + + "+0GjeuXDVzUqhKDDC3Sq4T2nwix4CjKvawnHC+vpHzRmdZSkXz3nrSdhJS5JnOap\n" + + "I4CdyYkP3jQs5P03dkv5RZAoqNPkeftwadu0cjZj8HEC8bVSd7YCS/0Gbgvp/eK3\n" + + "aOXZAWhELyQON4bbXiWzMTcO2AA5soBmP2QnBNdq7NxbEkBec8aAXZiyXn17JYit\n" + + "HRtJ7beptXjN0y2bEhOvsBHbDpjGO22fZfQ0aywP2k4/XanDf4WJolEFgLj2Qp8F\n" + + "pw1Olc2UhApxhAqjkme09hggli3wUhthQrpBlmntfbvBcmmO+p/jV8Zn\n" + + "=Roko\n" + + "-----END PGP MESSAGE-----\n"; + + private static final String PKESK3_SEIP_OPS23_LIT_SIG23 = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "wcDMA3wvqk35PDeyAQv/byYGOehnfgZu/HJSSDEQhYE27lh5j+oQPktYBxsTI3Ii\n" + + "AJ0pyJggtUM3c/e6z4HioAs4oQiaH1eoGBIl7noROhHJgT6E/oKdHJHmQndo0gT7\n" + + "xzSHKrEZYEqT45LD6jT5teOiwGX/B7as6jaQOT+Nh+M0ZqpPrBdWDeUoY/I/lx9j\n" + + "IcXQUhKuqJwZ16xsnv0JJ80rdp5qv0g2NHT1hK1JEONyT2fefov3EXaSpQZHRBEi\n" + + "XfwToHcJrgemFoGwZBQhXsPWBgKH92aW6r7ZJZMQ4BE2SwqEw+cbaaqwfFRJ9puj\n" + + "ZUBi2JGwnYImZyD7jYverkjH05vI7d5qQDhCT6GPD8Q0WKfY225LFTj/zzbC23lp\n" + + "VLlbT2Ap8ZFOESpM+crOUaOguBhnTOF05s1eXhQYxIKzJTW8UVrzbwI8ut3BnEDj\n" + + "0aDqUR+QDYgYmz8hnEjanHk2McvfaNdV6uOPZNph2RuPzhZeoNcz1PLy+XPFJsQT\n" + + "EdqURJ2D35qmrBvq6klN0sGYAXEtW4hxMaKhH7/SIk2m3kUAChjtzr4XRcE2i8h3\n" + + "9uD6CRWxokArfRVAp5RpXt3ywoYrl1Mp5prVwWcrTzYVPZwwe/bYAFTIfavi0Ezb\n" + + "A/ah2e1EYCTWxLY0Klzil2Xw9/Dc/JPTRqzWJxIn0AU4DVfYNwlH3QimDUDKOKbu\n" + + "bw4bLEEBKRr5BcNMA2rJOw/n3AmKxQcdSFJh3ZNtDPWzRwflIzE2qB686hpeOs34\n" + + "T2iJcfr9W9rKcYI7+WYcZA3fWokaWUfXdWrPMXBVJuPdGezGLSfe/OM4kw/8s2vR\n" + + "Bk88WZ1ZiFXE2CRaHP80fHFpxAZioWTuC5UGhF7NgZ7Q1E85GaGVe1fQeqmeX3mo\n" + + "gwAWwq9WFhPQQPwdrDz+1h/pzD0RVW7D+zfWdF//vesc4z1Bpi5prbMdpVdmyvO9\n" + + "8Rcc42GtmhtYSB9SPjzPuN8PrWvD3AKgw5vQro6oiNh0TGmj5Se4lXCfRfCl4tak\n" + + "cmvdi+1wAvn5OFdxYKKHNvjavwj2SY70nx0ACasBpbMEwoQ0StZmOxCaofkgvEwN\n" + + "t8jMq/MVrIfunhMjx0/GDpBZBb8kze8zrvWxlbTnoIfh2yVEqmTZWue9HX5Mnh8P\n" + + "wexxNrPaafTjA3nUgXXzlItJ/Wa43pYw2sgcBPlF/jRKSFiD+pDLysqwpH0ANsJ3\n" + + "G7t8Qavq1DlrHFgV5jhfR1tjA5ohjxx7yzceQBvZUFxKM1WEWR+9dRb3bpfZJr0g\n" + + "qgO36bpCeCEej+ubXpXXTN28LQLXjQHlE2o1NGLoGl+G72tXOTx30kPS\n" + + "=wUZn\n" + + "-----END PGP MESSAGE-----\n"; + + private static final String PKESK3_SEIP_OPS23_OPS3_LIT_SIG4_SIG23 = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "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\n" + + "=qFHk\n" + + "-----END PGP MESSAGE-----\n"; + + private static final String PKESK3_SEIP_OPS3_OPS23_LIT_SIG23_SIG4 = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "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\n" + + "=jGpv\n" + + "-----END PGP MESSAGE-----\n"; + + private final PGPSecretKeyRing key; + private final PGPPublicKeyRing cert; + + public UnsupportedPacketVersionsTest() throws IOException { + key = PGPainless.readKeyRing().secretKeyRing(KEY); + cert = PGPainless.extractCertificate(key); + } + + @Test + public void pkesk3_pkesk23_seip() throws PGPException, IOException { + decryptAndCompare(PKESK3_PKESK23_SEIP, "Encrypted using SEIP + MDC."); + } + + @Test + public void pkesk23_pkesk_seip() throws PGPException, IOException { + decryptAndCompare(PKESK23_PKESK3_SEIP, "Encrypted using SEIP + MDC."); + } + + @Test + public void pkesk3_skesk23_seip() throws PGPException, IOException { + decryptAndCompare(PKESK3_SKESK23_SEIP, "Encrypted using SEIP + MDC."); + } + + @Test + public void skesk23_pkesk3_seip() throws PGPException, IOException { + decryptAndCompare(SKESK23_PKESK3_SEIP, "Encrypted using SEIP + MDC."); + } + + @Test + public void pkesk3_skesk4Ws2k23_seip() throws PGPException, IOException { + decryptAndCompare(PKESK3_SKESK4wS2K23_SEIP, "Encrypted using SEIP + MDC."); + } + + @Test + public void skesk4Ws2k23_pkesk3_seip() throws PGPException, IOException { + decryptAndCompare(SKESK4wS2K23_PKESK3_SEIP, "Encrypted using SEIP + MDC."); + } + + @Test + public void pkesk3_seip_ops3_lit_sig4() throws PGPException, IOException { + decryptAndCompare(PKESK3_SEIP_OPS3_LIT_SIG4, "Encrypted, signed message."); + } + + @Test + public void pkesk3_seip_ops23_lit_sig23() throws PGPException, IOException { + decryptAndCompare(PKESK3_SEIP_OPS23_LIT_SIG23, "Encrypted, signed message."); + } + + @Test + public void pkesk3_seip_ops23_ops3_lit_sig4_sig23() throws PGPException, IOException { + decryptAndCompare(PKESK3_SEIP_OPS23_OPS3_LIT_SIG4_SIG23, "Encrypted, signed message."); + } + + @Test + public void pkesk3_seip_ops3_ops23_lit_sig23_sig4() throws PGPException, IOException { + decryptAndCompare(PKESK3_SEIP_OPS3_OPS23_LIT_SIG23_SIG4, "Encrypted, signed message."); + } + + public void decryptAndCompare(String msg, String plain) throws IOException, PGPException { + // noinspection CharsetObjectCanBeUsed + ByteArrayInputStream inputStream = new ByteArrayInputStream(msg.getBytes(Charset.forName("UTF8"))); + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(inputStream) + .withOptions(ConsumerOptions.get() + .addDecryptionKey(key) + .addVerificationCert(cert)); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Streams.pipeAll(decryptionStream, out); + decryptionStream.close(); + + assertEquals(plain, out.toString()); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyDetachedSignatureTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyDetachedSignatureTest.java new file mode 100644 index 00000000..e1406f87 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyDetachedSignatureTest.java @@ -0,0 +1,146 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.decryption_verification.cleartext_signatures.InMemoryMultiPassStrategy; + +public class VerifyDetachedSignatureTest { + + @Test + public void test1() throws PGPException, IOException { + String signedContent = "Content-Type: multipart/mixed; boundary=\"OSR6TONWKJD9dgyc2XH5AQPNnAs7pdg1t\"\n" + + "\n" + + "--OSR6TONWKJD9dgyc2XH5AQPNnAs7pdg1t\n" + + "Content-Type: text/plain; charset=utf-8\n" + + "Content-Transfer-Encoding: quoted-printable\n" + + "Content-Language: en-US\n" + + "\n" + + "NOT encrypted + signed(detached)\n" + + "\n" + + "\n" + + "\n" + + "--OSR6TONWKJD9dgyc2XH5AQPNnAs7pdg1t--\n"; + String signature = "-----BEGIN PGP SIGNATURE-----\n" + + "\n" + + "iHUEARYIAB0WIQTBZCjWAcs5N4nPYdTDIInNavjWzgUCYgKPzAAKCRDDIInNavjW\n" + + "zmdoAP0TdFt1OWqosHhXxt2hNYqZQMc6bgQRpJNL029nRyzkPAD/SoYJ4T+aYEhw\n" + + "11qrbXloqkr0G3QaA6/zk31RPMI/bgI=\n" + + "=o5Ze\n" + + "-----END PGP SIGNATURE-----\n"; + String pubkey = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "\n" + + "mDMEYIucWBYJKwYBBAHaRw8BAQdAew+8mzMWyf3+Pfy49qa60uKV6e5os7de4TdZ\n" + + "ceAWUq+0F2RlbmJvbmQ3QGZsb3djcnlwdC50ZXN0iHgEExYKACAFAmCLnFgCGwMF\n" + + "FgIDAQAECwkIBwUVCgkICwIeAQIZAQAKCRDDIInNavjWzm3JAQCgFgCEyD58iEa/\n" + + "Rw/DYNoQNoZC1lhw1bxBiOcIbtkdBgEAsDFZu3TBavOMKI7KW+vfMBHtRVbkMNpv\n" + + "unaAldoabgO4OARgi5xYEgorBgEEAZdVAQUBAQdAB1/Mrq5JGYim4KqGTSK4OESQ\n" + + "UwPgK56q0yrkiU9WgyYDAQgHiHUEGBYKAB0FAmCLnFgCGwwFFgIDAQAECwkIBwUV\n" + + "CgkICwIeAQAKCRDDIInNavjWzjMgAQCU+R1fItqdY6lt9jXUqipmXuqVaEFPwNA8\n" + + "YJ1rIwDwVQEAyUc8162KWzA2iQB5akwLwNr/pLDDtOWwhLUkrBb3mAc=\n" + + "=pXF6\n" + + "-----END PGP PUBLIC KEY BLOCK-----\n"; + + + DecryptionStream verifier = PGPainless.decryptAndOrVerify() + .onInputStream(new ByteArrayInputStream(signedContent.getBytes(StandardCharsets.UTF_8))) + .withOptions( + new ConsumerOptions() + .addVerificationOfDetachedSignatures(new ByteArrayInputStream(signature.getBytes(StandardCharsets.UTF_8))) + .addVerificationCerts(PGPainless.readKeyRing().keyRingCollection(pubkey, true).getPgpPublicKeyRingCollection()) + .setMultiPassStrategy(new InMemoryMultiPassStrategy()) + ); + + Streams.drain(verifier); + verifier.close(); + MessageMetadata metadata = verifier.getMetadata(); + assertTrue(metadata.isVerifiedSigned()); + } + + @Test + public void test2() throws PGPException, IOException { + String signedContent = "Content-Type: multipart/mixed; boundary=\"------------26m0wPaTDf7nRDIftnMj4qjE\";\r\n" + + " protected-headers=\"v1\"\r\n" + + "From: Denys \r\n" + + "To: default@flowcrypt.test\r\n" + + "Message-ID: \r\n" + + "Subject: Signed + pub key\r\n" + + "\r\n" + + "--------------26m0wPaTDf7nRDIftnMj4qjE\r\n" + + "Content-Type: multipart/mixed; boundary=\"------------RQxi6oNuQI1n8MnuNglORR5s\"\r\n" + + "\r\n" + + "--------------RQxi6oNuQI1n8MnuNglORR5s\r\n" + + "Content-Type: text/plain; charset=UTF-8; format=flowed\r\n" + + "Content-Transfer-Encoding: base64\r\n" + + "\r\n" + + "U29tZSBpbXBvcnRhbnQgdGV4dA0KDQo=\r\n" + + "--------------RQxi6oNuQI1n8MnuNglORR5s\r\n" + + "Content-Type: application/pgp-keys; name=\"OpenPGP_0xC32089CD6AF8D6CE.asc\"\r\n" + + "Content-Disposition: attachment; filename=\"OpenPGP_0xC32089CD6AF8D6CE.asc\"\r\n" + + "Content-Description: OpenPGP public key\r\n" + + "Content-Transfer-Encoding: quoted-printable\r\n" + + "\r\n" + + "-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n" + + "\r\n" + + "xjMEYIucWBYJKwYBBAHaRw8BAQdAew+8mzMWyf3+Pfy49qa60uKV6e5os7de4TdZ\r\n" + + "ceAWUq/NF2RlbmJvbmQ3QGZsb3djcnlwdC50ZXN0wngEExYKACAFAmCLnFgCGwMF\r\n" + + "FgIDAQAECwkIBwUVCgkICwIeAQIZAQAKCRDDIInNavjWzm3JAQCgFgCEyD58iEa/\r\n" + + "Rw/DYNoQNoZC1lhw1bxBiOcIbtkdBgEAsDFZu3TBavOMKI7KW+vfMBHtRVbkMNpv\r\n" + + "unaAldoabgPOOARgi5xYEgorBgEEAZdVAQUBAQdAB1/Mrq5JGYim4KqGTSK4OESQ\r\n" + + "UwPgK56q0yrkiU9WgyYDAQgHwnUEGBYKAB0FAmCLnFgCGwwFFgIDAQAECwkIBwUV\r\n" + + "CgkICwIeAQAKCRDDIInNavjWzjMgAQCU+R1fItqdY6lt9jXUqipmXuqVaEFPwNA8\r\n" + + "YJ1rIwDwVQEAyUc8162KWzA2iQB5akwLwNr/pLDDtOWwhLUkrBb3mAc=3D\r\n" + + "=3DyJxA\r\n" + + "-----END PGP PUBLIC KEY BLOCK-----\r\n" + + "\r\n" + + "--------------RQxi6oNuQI1n8MnuNglORR5s--\r\n" + + "\r\n" + + "--------------26m0wPaTDf7nRDIftnMj4qjE--\r\n"; + String signature = "-----BEGIN PGP SIGNATURE-----\n" + + "\n" + + "wnsEABYIACMWIQTBZCjWAcs5N4nPYdTDIInNavjWzgUCYguNRQUDAAAAAAAKCRDDIInNavjWzoxf\n" + + "AQCOCu6bityLBbY1MPF+smwYLjkJvzEHf+ErXC7KkI4mnAEAn7FPPOzJAwWENv8a//0zg4P9Ymdr\n" + + "uyp1EJ1tsavXRQA=\n" + + "=K5yW\n" + + "-----END PGP SIGNATURE-----\n"; + String pubkey = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "\n" + + "mDMEYIucWBYJKwYBBAHaRw8BAQdAew+8mzMWyf3+Pfy49qa60uKV6e5os7de4TdZ\n" + + "ceAWUq+0F2RlbmJvbmQ3QGZsb3djcnlwdC50ZXN0iHgEExYKACAFAmCLnFgCGwMF\n" + + "FgIDAQAECwkIBwUVCgkICwIeAQIZAQAKCRDDIInNavjWzm3JAQCgFgCEyD58iEa/\n" + + "Rw/DYNoQNoZC1lhw1bxBiOcIbtkdBgEAsDFZu3TBavOMKI7KW+vfMBHtRVbkMNpv\n" + + "unaAldoabgO4OARgi5xYEgorBgEEAZdVAQUBAQdAB1/Mrq5JGYim4KqGTSK4OESQ\n" + + "UwPgK56q0yrkiU9WgyYDAQgHiHUEGBYKAB0FAmCLnFgCGwwFFgIDAQAECwkIBwUV\n" + + "CgkICwIeAQAKCRDDIInNavjWzjMgAQCU+R1fItqdY6lt9jXUqipmXuqVaEFPwNA8\n" + + "YJ1rIwDwVQEAyUc8162KWzA2iQB5akwLwNr/pLDDtOWwhLUkrBb3mAc=\n" + + "=pXF6\n" + + "-----END PGP PUBLIC KEY BLOCK-----\n"; + + DecryptionStream verifier = PGPainless.decryptAndOrVerify() + .onInputStream(new ByteArrayInputStream(signedContent.getBytes(StandardCharsets.UTF_8))) + .withOptions( + new ConsumerOptions() + .addVerificationOfDetachedSignatures(new ByteArrayInputStream(signature.getBytes(StandardCharsets.UTF_8))) + .addVerificationCerts(PGPainless.readKeyRing().keyRingCollection(pubkey, true).getPgpPublicKeyRingCollection()) + .setMultiPassStrategy(new InMemoryMultiPassStrategy()) + ); + + Streams.drain(verifier); + verifier.close(); + MessageMetadata metadata = verifier.getMetadata(); + assertTrue(metadata.isVerifiedSigned()); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyNotBeforeNotAfterTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyNotBeforeNotAfterTest.java index d67b3d95..069a5f2d 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyNotBeforeNotAfterTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyNotBeforeNotAfterTest.java @@ -68,8 +68,8 @@ public class VerifyNotBeforeNotAfterTest { .onInputStream(new ByteArrayInputStream(inlineSigned)) .withOptions(options); - OpenPgpMetadata metadata = processSignedData(verifier); - assertTrue(metadata.getVerifiedSignatures().containsKey(new SubkeyIdentifier(certificate))); + MessageMetadata metadata = processSignedData(verifier); + assertTrue(metadata.isVerifiedSignedBy(certificate)); } @Test @@ -81,8 +81,8 @@ public class VerifyNotBeforeNotAfterTest { .onInputStream(new ByteArrayInputStream(data)) .withOptions(options); - OpenPgpMetadata metadata = processSignedData(verifier); - assertTrue(metadata.containsVerifiedSignatureFrom(certificate)); + MessageMetadata metadata = processSignedData(verifier); + assertTrue(metadata.isVerifiedSignedBy(certificate)); } @Test @@ -93,8 +93,8 @@ public class VerifyNotBeforeNotAfterTest { DecryptionStream verifier = PGPainless.decryptAndOrVerify() .onInputStream(new ByteArrayInputStream(inlineSigned)) .withOptions(options); - OpenPgpMetadata metadata = processSignedData(verifier); - assertTrue(metadata.getVerifiedSignatures().containsKey(signingKey)); + MessageMetadata metadata = processSignedData(verifier); + assertTrue(metadata.isVerifiedSignedBy(certificate)); } @Test @@ -106,8 +106,8 @@ public class VerifyNotBeforeNotAfterTest { DecryptionStream verifier = PGPainless.decryptAndOrVerify() .onInputStream(new ByteArrayInputStream(data)) .withOptions(options); - OpenPgpMetadata metadata = processSignedData(verifier); - assertTrue(metadata.getVerifiedSignatures().containsKey(signingKey)); + MessageMetadata metadata = processSignedData(verifier); + assertTrue(metadata.isVerifiedSignedBy(certificate)); } @Test @@ -118,8 +118,8 @@ public class VerifyNotBeforeNotAfterTest { DecryptionStream verifier = PGPainless.decryptAndOrVerify() .onInputStream(new ByteArrayInputStream(inlineSigned)) .withOptions(options); - OpenPgpMetadata metadata = processSignedData(verifier); - assertFalse(metadata.getVerifiedSignatures().containsKey(signingKey)); + MessageMetadata metadata = processSignedData(verifier); + assertFalse(metadata.isVerifiedInlineSignedBy(certificate)); } @Test @@ -131,8 +131,8 @@ public class VerifyNotBeforeNotAfterTest { DecryptionStream verifier = PGPainless.decryptAndOrVerify() .onInputStream(new ByteArrayInputStream(data)) .withOptions(options); - OpenPgpMetadata metadata = processSignedData(verifier); - assertFalse(metadata.getVerifiedSignatures().containsKey(signingKey)); + MessageMetadata metadata = processSignedData(verifier); + assertFalse(metadata.isVerifiedSignedBy(certificate)); } @Test @@ -143,8 +143,8 @@ public class VerifyNotBeforeNotAfterTest { DecryptionStream verifier = PGPainless.decryptAndOrVerify() .onInputStream(new ByteArrayInputStream(inlineSigned)) .withOptions(options); - OpenPgpMetadata metadata = processSignedData(verifier); - assertTrue(metadata.getVerifiedSignatures().containsKey(signingKey)); + MessageMetadata metadata = processSignedData(verifier); + assertTrue(metadata.isVerifiedSignedBy(certificate)); } @Test @@ -156,8 +156,8 @@ public class VerifyNotBeforeNotAfterTest { DecryptionStream verifier = PGPainless.decryptAndOrVerify() .onInputStream(new ByteArrayInputStream(data)) .withOptions(options); - OpenPgpMetadata metadata = processSignedData(verifier); - assertTrue(metadata.getVerifiedSignatures().containsKey(signingKey)); + MessageMetadata metadata = processSignedData(verifier); + assertTrue(metadata.isVerifiedSignedBy(certificate)); } @Test @@ -168,8 +168,8 @@ public class VerifyNotBeforeNotAfterTest { DecryptionStream verifier = PGPainless.decryptAndOrVerify() .onInputStream(new ByteArrayInputStream(inlineSigned)) .withOptions(options); - OpenPgpMetadata metadata = processSignedData(verifier); - assertFalse(metadata.getVerifiedSignatures().containsKey(signingKey)); + MessageMetadata metadata = processSignedData(verifier); + assertFalse(metadata.isVerifiedSignedBy(certificate)); } @Test @@ -181,13 +181,13 @@ public class VerifyNotBeforeNotAfterTest { DecryptionStream verifier = PGPainless.decryptAndOrVerify() .onInputStream(new ByteArrayInputStream(data)) .withOptions(options); - OpenPgpMetadata metadata = processSignedData(verifier); - assertFalse(metadata.getVerifiedSignatures().containsKey(signingKey)); + MessageMetadata metadata = processSignedData(verifier); + assertFalse(metadata.isVerifiedSignedBy(certificate)); } - private OpenPgpMetadata processSignedData(DecryptionStream verifier) throws IOException { + private MessageMetadata processSignedData(DecryptionStream verifier) throws IOException { Streams.drain(verifier); verifier.close(); - return verifier.getResult(); + return verifier.getMetadata(); } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifySignatureByCertificationKeyFailsTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifySignatureByCertificationKeyFailsTest.java new file mode 100644 index 00000000..3d2ea092 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifySignatureByCertificationKeyFailsTest.java @@ -0,0 +1,228 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification; + +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +public class VerifySignatureByCertificationKeyFailsTest { + + // Key with non-signing primary key and dedicated signing subkey + private static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: ACFC 7FFA 02BE C1E8 5002 2D35 AFE4 67B2 9A41 A0CE\n" + + "Comment: Complex RSA \n" + + "\n" + + "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\n" + + "=hhYV\n" + + "-----END PGP PRIVATE KEY BLOCK-----\n"; + + private static byte[] DATA = "Hello, World!".getBytes(StandardCharsets.UTF_8); + + // Signature by primary key (shall be rejected, as the primary key is not signing capable. + // We instead expect the signing subkey to sign). + private static final String SIG = "-----BEGIN PGP SIGNATURE-----\n" + + "Version: PGPainless\n" + + "\n" + + "iQIcBAABCAAGBQJmEAYYAAoJEK/kZ7KaQaDOH4MP/2kaK8lQaBU+jChpWPLR2R2+\n" + + "dB7j29tFPRAqbzbazxaF+jZQxuuHWtM3bwd9Vdta9zirDc27b7XyufFLBza4Bn+R\n" + + "7fT7uHTQQts/zaX8YGxJ90rb06toFXiv/rlm531kLaGXxlACU6SpI8maqpP4im+G\n" + + "W0LgBDZiT9udFs3eeJZ8O3yDLP29Rdw8sHPa6pOyyhkkhsvo0bNaBaSt6GDW5UK9\n" + + "f5Gz+XF9ZLJgsNqQwWQM55+4ZhdkfEszRcJgAhuSCamk+ZLfvIPhEu21/7weNq3c\n" + + "Yp0hvaz27gaW7IkjEgI1FqkPrmJmyk5SVMMvaev9p0WXDgUIeDLI6CwvoXaoMCAX\n" + + "pg0Q754ccHu2pELwNb5YIxGSPSMXRVH8xUDqicZpl/50ucy3g348s5HekcVnzBtX\n" + + "UKVX3tU6r5HrkVAX7bDGht3WXE1jRE98W3uKpFWzrJBK+uQIyOtOEXeKT+z1BbNy\n" + + "CvYeDq4xjpGYB2tJY2LKXrC4+IzJ56e3XU2t75KhO0SBV+Ax1bJ0MBnmAedg0pg5\n" + + "0r+mknBqoYu+OHNwMS/N+YH1iZEV0QxP+ldLp+ff2QiIvtiDcOIQ0oNEJy0bqh0p\n" + + "TrS9PKMl+kH+FaAPUVb0ruviTd/zPljjiJ2P396bu1JBXdoncn2y4KklQOWHJSVI\n" + + "F5ZjatEBixl6pdW7I5Cr\n" + + "=vPG/\n" + + "-----END PGP SIGNATURE-----"; + + @Test + public void testSignatureByNonSigningPrimaryKeyIsRejected() throws Exception { + PGPSecretKeyRing key = PGPainless.readKeyRing().secretKeyRing(KEY); + + DecryptionStream verifier = PGPainless.decryptAndOrVerify() + .onInputStream(new ByteArrayInputStream(DATA)) + .withOptions(ConsumerOptions.get() + .addVerificationCert(PGPainless.extractCertificate(key)) + .addVerificationOfDetachedSignatures(new ByteArrayInputStream(SIG.getBytes(StandardCharsets.UTF_8)))); + + Streams.drain(verifier); + verifier.close(); + + MessageMetadata result = verifier.getMetadata(); + assertFalse(result.isVerifiedSigned()); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyVersion3SignaturePacketTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyVersion3SignaturePacketTest.java new file mode 100644 index 00000000..6b9d9cab --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyVersion3SignaturePacketTest.java @@ -0,0 +1,69 @@ +// SPDX-FileCopyrightText: 2023 Bastien Jansen +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPrivateKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.PGPV3SignatureGenerator; +import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder; +import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.HashAlgorithm; +import org.pgpainless.algorithm.PublicKeyAlgorithm; +import org.pgpainless.algorithm.SignatureType; +import org.pgpainless.implementation.ImplementationFactory; +import org.pgpainless.key.TestKeys; +import org.pgpainless.key.protection.SecretKeyRingProtector; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +class VerifyVersion3SignaturePacketTest { + + + protected static final byte[] DATA = "hello".getBytes(StandardCharsets.UTF_8); + + @Test + void verifyDetachedVersion3Signature() throws PGPException, IOException { + PGPSignature version3Signature = generateV3Signature(); + + ConsumerOptions options = new ConsumerOptions() + .addVerificationCert(TestKeys.getEmilPublicKeyRing()) + .addVerificationOfDetachedSignatures(new ByteArrayInputStream(version3Signature.getEncoded())); + + DecryptionStream verifier = PGPainless.decryptAndOrVerify() + .onInputStream(new ByteArrayInputStream(DATA)) + .withOptions(options); + + MessageMetadata metadata = processSignedData(verifier); + assertTrue(metadata.isVerifiedSignedBy(TestKeys.getEmilPublicKeyRing())); + } + + private static PGPSignature generateV3Signature() throws IOException, PGPException { + PGPContentSignerBuilder builder = ImplementationFactory.getInstance().getPGPContentSignerBuilder(PublicKeyAlgorithm.ECDSA, HashAlgorithm.SHA512); + PGPV3SignatureGenerator signatureGenerator = new PGPV3SignatureGenerator(builder); + + PGPSecretKeyRing secretKeys = TestKeys.getEmilSecretKeyRing(); + SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); + PGPPrivateKey privateKey = secretKeys.getSecretKey().extractPrivateKey(protector.getDecryptor(secretKeys.getSecretKey().getKeyID())); + + signatureGenerator.init(SignatureType.CANONICAL_TEXT_DOCUMENT.getCode(), privateKey); + signatureGenerator.update(DATA); + + return signatureGenerator.generate(); + } + + private MessageMetadata processSignedData(DecryptionStream verifier) throws IOException { + Streams.drain(verifier); + verifier.close(); + return verifier.getMetadata(); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyWithMissingPublicKeyCallback.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyWithMissingPublicKeyCallbackTest.java similarity index 90% rename from pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyWithMissingPublicKeyCallback.java rename to pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyWithMissingPublicKeyCallbackTest.java index 9c786807..0d58e7dd 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyWithMissingPublicKeyCallback.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyWithMissingPublicKeyCallbackTest.java @@ -14,8 +14,6 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKey; @@ -38,11 +36,11 @@ import org.pgpainless.key.util.KeyRingUtils; * a signature is encountered which was made by a key that was not provided in * {@link ConsumerOptions#addVerificationCert(PGPPublicKeyRing)}. */ -public class VerifyWithMissingPublicKeyCallback { +public class VerifyWithMissingPublicKeyCallbackTest { @Test public void testMissingPublicKeyCallback() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { - PGPSecretKeyRing signingSecKeys = PGPainless.generateKeyRing().modernKeyRing("alice", null); + PGPSecretKeyRing signingSecKeys = PGPainless.generateKeyRing().modernKeyRing("alice"); PGPPublicKey signingKey = new KeyRingInfo(signingSecKeys).getSigningSubkeys().get(0); PGPPublicKeyRing signingPubKeys = KeyRingUtils.publicKeyRingFrom(signingSecKeys); PGPPublicKeyRing unrelatedKeys = TestKeys.getJulietPublicKeyRing(); @@ -63,9 +61,8 @@ public class VerifyWithMissingPublicKeyCallback { .withOptions(new ConsumerOptions() .addVerificationCert(unrelatedKeys) .setMissingCertificateCallback(new MissingPublicKeyCallback() { - @Nullable @Override - public PGPPublicKeyRing onMissingPublicKeyEncountered(@Nonnull Long keyId) { + public PGPPublicKeyRing onMissingPublicKeyEncountered(long keyId) { assertEquals(signingKey.getKeyID(), keyId, "Signing key-ID mismatch."); return signingPubKeys; } @@ -76,7 +73,7 @@ public class VerifyWithMissingPublicKeyCallback { verificationStream.close(); assertArrayEquals(msg.getBytes(StandardCharsets.UTF_8), plainOut.toByteArray()); - OpenPgpMetadata metadata = verificationStream.getResult(); - assertTrue(metadata.containsVerifiedSignatureFrom(signingPubKeys)); + MessageMetadata metadata = verificationStream.getMetadata(); + assertTrue(metadata.isVerifiedSignedBy(signingPubKeys)); } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/WrongSignerUserIdTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/WrongSignerUserIdTest.java new file mode 100644 index 00000000..f3336373 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/WrongSignerUserIdTest.java @@ -0,0 +1,174 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.Date; +import java.util.Iterator; + +import org.bouncycastle.bcpg.ArmoredOutputStream; +import org.bouncycastle.bcpg.BCPGOutputStream; +import org.bouncycastle.bcpg.CompressionAlgorithmTags; +import org.bouncycastle.bcpg.SymmetricKeyAlgorithmTags; +import org.bouncycastle.openpgp.PGPCompressedDataGenerator; +import org.bouncycastle.openpgp.PGPEncryptedDataGenerator; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPLiteralDataGenerator; +import org.bouncycastle.openpgp.PGPPrivateKey; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.PGPSignatureGenerator; +import org.bouncycastle.openpgp.PGPSignatureSubpacketGenerator; +import org.bouncycastle.openpgp.operator.PGPDataEncryptorBuilder; +import org.bouncycastle.openpgp.operator.bc.BcPGPContentSignerBuilder; +import org.bouncycastle.openpgp.operator.bc.BcPGPDataEncryptorBuilder; +import org.bouncycastle.openpgp.operator.bc.BcPublicKeyKeyEncryptionMethodGenerator; +import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.HashAlgorithm; +import org.pgpainless.key.protection.UnlockSecretKey; +import org.pgpainless.policy.Policy; +import org.pgpainless.util.Passphrase; + +public class WrongSignerUserIdTest { + + private static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + " Comment: Alice's OpenPGP Transferable Secret Key\n" + + " Comment: https://www.ietf.org/id/draft-bre-openpgp-samples-01.html\n" + + "\n" + + " lFgEXEcE6RYJKwYBBAHaRw8BAQdArjWwk3FAqyiFbFBKT4TzXcVBqPTB3gmzlC/U\n" + + " b7O1u10AAP9XBeW6lzGOLx7zHH9AsUDUTb2pggYGMzd0P3ulJ2AfvQ4RtCZBbGlj\n" + + " ZSBMb3ZlbGFjZSA8YWxpY2VAb3BlbnBncC5leGFtcGxlPoiQBBMWCAA4AhsDBQsJ\n" + + " CAcCBhUKCQgLAgQWAgMBAh4BAheAFiEE64W7X6M6deFelE5j8jFVDE9H444FAl2l\n" + + " nzoACgkQ8jFVDE9H447pKwD6A5xwUqIDprBzrHfahrImaYEZzncqb25vkLV2arYf\n" + + " a78A/R3AwtLQvjxwLDuzk4dUtUwvUYibL2sAHwj2kGaHnfICnF0EXEcE6RIKKwYB\n" + + " BAGXVQEFAQEHQEL/BiGtq0k84Km1wqQw2DIikVYrQrMttN8d7BPfnr4iAwEIBwAA\n" + + " /3/xFPG6U17rhTuq+07gmEvaFYKfxRB6sgAYiW6TMTpQEK6IeAQYFggAIBYhBOuF\n" + + " u1+jOnXhXpROY/IxVQxPR+OOBQJcRwTpAhsMAAoJEPIxVQxPR+OOWdABAMUdSzpM\n" + + " hzGs1O0RkWNQWbUzQ8nUOeD9wNbjE3zR+yfRAQDbYqvtWQKN4AQLTxVJN5X5AWyb\n" + + " Pnn+We1aTBhaGa86AQ==\n" + + " =n8OM\n" + + " -----END PGP PRIVATE KEY BLOCK-----"; + private static final String USER_ID = "Alice Lovelace "; + + @Test + public void verificationSucceedsWithDisabledCheck() throws PGPException, IOException { + executeTest(false, true); + } + + @Test + public void verificationFailsWithEnabledCheck() throws PGPException, IOException { + executeTest(true, false); + } + + @AfterAll + public static void resetDefault() { + PGPainless.getPolicy().setSignerUserIdValidationLevel(Policy.SignerUserIdValidationLevel.DISABLED); + } + + public void executeTest(boolean enableCheck, boolean expectSucessfulVerification) throws IOException, PGPException { + PGPainless.getPolicy().setSignerUserIdValidationLevel(enableCheck ? Policy.SignerUserIdValidationLevel.STRICT : Policy.SignerUserIdValidationLevel.DISABLED); + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); + assertEquals(USER_ID, secretKeys.getPublicKey().getUserIDs().next()); + + String messageWithWrongUserId = generateTestMessage(secretKeys); + verifyTestMessage(messageWithWrongUserId, secretKeys, expectSucessfulVerification); + } + + private void verifyTestMessage(String messageWithWrongUserId, PGPSecretKeyRing secretKeys, boolean expectSuccessfulVerification) throws IOException, PGPException { + PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKeys); + + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify().onInputStream( + new ByteArrayInputStream(messageWithWrongUserId.getBytes(StandardCharsets.UTF_8))) + .withOptions(new ConsumerOptions() + .addDecryptionKey(secretKeys) + .addVerificationCert(certificate)); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Streams.pipeAll(decryptionStream, out); + + decryptionStream.close(); + MessageMetadata metadata = decryptionStream.getMetadata(); + + if (expectSuccessfulVerification) { + assertTrue(metadata.isVerifiedSigned()); + } else { + assertFalse(metadata.isVerifiedSigned()); + } + + } + + private String generateTestMessage(PGPSecretKeyRing secretKeys) throws PGPException, IOException { + PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKeys); + + assertEquals(USER_ID, certificate.getPublicKey().getUserIDs().next()); + + Iterator keys = secretKeys.getSecretKeys(); + PGPSecretKey signingKey = keys.next(); + PGPSecretKey encryptionKey = keys.next(); + + PGPPrivateKey signingPrivKey = UnlockSecretKey.unlockSecretKey(signingKey, Passphrase.emptyPassphrase()); + + // ARMOR + ByteArrayOutputStream cipherText = new ByteArrayOutputStream(); + ArmoredOutputStream armorOut = new ArmoredOutputStream(cipherText); + + // ENCRYPTION + PGPDataEncryptorBuilder dataEncryptorBuilder = new BcPGPDataEncryptorBuilder(SymmetricKeyAlgorithmTags.AES_256); + dataEncryptorBuilder.setWithIntegrityPacket(true); + + PGPEncryptedDataGenerator encDataGenerator = new PGPEncryptedDataGenerator(dataEncryptorBuilder); + encDataGenerator.addMethod(new BcPublicKeyKeyEncryptionMethodGenerator(encryptionKey.getPublicKey())); + OutputStream encStream = encDataGenerator.open(armorOut, new byte[4096]); + + // COMPRESSION + PGPCompressedDataGenerator compressedDataGenerator = new PGPCompressedDataGenerator(CompressionAlgorithmTags.ZLIB); + BCPGOutputStream bOut = new BCPGOutputStream(compressedDataGenerator.open(encStream)); + + // SIGNING + PGPSignatureGenerator sigGen = new PGPSignatureGenerator( + new BcPGPContentSignerBuilder(signingKey.getPublicKey().getAlgorithm(), HashAlgorithm.SHA512.getAlgorithmId())); + sigGen.init(PGPSignature.BINARY_DOCUMENT, signingPrivKey); + + PGPSignatureSubpacketGenerator subpacketGenerator = new PGPSignatureSubpacketGenerator(); + subpacketGenerator.addSignerUserID(false, "Albert Lovelace "); + sigGen.setHashedSubpackets(subpacketGenerator.generate()); + + sigGen.generateOnePassVersion(false).encode(bOut); + + // LITERAL DATA + PGPLiteralDataGenerator literalDataGenerator = new PGPLiteralDataGenerator(); + OutputStream lOut = literalDataGenerator.open(bOut, PGPLiteralDataGenerator.BINARY, + PGPLiteralDataGenerator.CONSOLE, new Date(), new byte[4096]); + + // write msg + ByteArrayInputStream msgIn = new ByteArrayInputStream("Hello, World!\n".getBytes(StandardCharsets.UTF_8)); + int ch; + while ((ch = msgIn.read()) >= 0) { + lOut.write(ch); + sigGen.update((byte) ch); + } + + lOut.close(); + sigGen.generate().encode(bOut); + compressedDataGenerator.close(); + encStream.close(); + armorOut.close(); + + return cipherText.toString(); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java new file mode 100644 index 00000000..10f8dceb --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java @@ -0,0 +1,197 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification.syntax_check; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; +import org.pgpainless.exception.MalformedOpenPgpMessageException; + +public class PDATest { + + + /** + * MSG is valid. + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testSimpleLiteralMessageIsValid() throws MalformedOpenPgpMessageException { + PDA check = new PDA(); + check.next(InputSymbol.LITERAL_DATA); + check.next(InputSymbol.END_OF_SEQUENCE); + + assertTrue(check.isValid()); + } + + /** + * OPS MSG SIG is valid. + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testSimpleOpsSignedMesssageIsValid() throws MalformedOpenPgpMessageException { + PDA check = new PDA(); + check.next(InputSymbol.ONE_PASS_SIGNATURE); + check.next(InputSymbol.LITERAL_DATA); + check.next(InputSymbol.SIGNATURE); + check.next(InputSymbol.END_OF_SEQUENCE); + + assertTrue(check.isValid()); + } + + + /** + * SIG MSG is valid. + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testSimplePrependSignedMessageIsValid() throws MalformedOpenPgpMessageException { + PDA check = new PDA(); + check.next(InputSymbol.SIGNATURE); + check.next(InputSymbol.LITERAL_DATA); + check.next(InputSymbol.END_OF_SEQUENCE); + + assertTrue(check.isValid()); + } + + + /** + * OPS COMP(MSG) SIG is valid. + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testOPSSignedCompressedMessageIsValid() throws MalformedOpenPgpMessageException { + PDA check = new PDA(); + check.next(InputSymbol.ONE_PASS_SIGNATURE); + check.next(InputSymbol.COMPRESSED_DATA); + // Here would be a nested PDA for the LiteralData packet + check.next(InputSymbol.SIGNATURE); + check.next(InputSymbol.END_OF_SEQUENCE); + + assertTrue(check.isValid()); + } + + @Test + public void testOPSSignedEncryptedMessageIsValid() { + PDA check = new PDA(); + check.next(InputSymbol.ONE_PASS_SIGNATURE); + check.next(InputSymbol.ENCRYPTED_DATA); + check.next(InputSymbol.SIGNATURE); + check.next(InputSymbol.END_OF_SEQUENCE); + assertTrue(check.isValid()); + } + + @Test + public void anyInputAfterEOSIsNotValid() { + PDA check = new PDA(); + check.next(InputSymbol.LITERAL_DATA); + check.next(InputSymbol.END_OF_SEQUENCE); + assertThrows(MalformedOpenPgpMessageException.class, + () -> check.next(InputSymbol.SIGNATURE)); + } + + @Test + public void testEncryptedMessageWithAppendedStandaloneSigIsNotValid() { + PDA check = new PDA(); + check.next(InputSymbol.ENCRYPTED_DATA); + assertThrows(MalformedOpenPgpMessageException.class, + () -> check.next(InputSymbol.SIGNATURE)); + } + + @Test + public void testOPSSignedEncryptedMessageWithMissingSigIsNotValid() { + PDA check = new PDA(); + check.next(InputSymbol.ONE_PASS_SIGNATURE); + check.next(InputSymbol.ENCRYPTED_DATA); + assertThrows(MalformedOpenPgpMessageException.class, + () -> check.next(InputSymbol.END_OF_SEQUENCE)); + } + + @Test + public void testTwoLiteralDataIsNotValid() { + PDA check = new PDA(); + check.next(InputSymbol.LITERAL_DATA); + assertThrows(MalformedOpenPgpMessageException.class, + () -> check.next(InputSymbol.LITERAL_DATA)); + } + + @Test + public void testTrailingSigIsNotValid() { + PDA check = new PDA(); + check.next(InputSymbol.LITERAL_DATA); + assertThrows(MalformedOpenPgpMessageException.class, + () -> check.next(InputSymbol.SIGNATURE)); + } + + @Test + public void testOPSAloneIsNotValid() { + PDA check = new PDA(); + check.next(InputSymbol.ONE_PASS_SIGNATURE); + assertThrows(MalformedOpenPgpMessageException.class, + () -> check.next(InputSymbol.END_OF_SEQUENCE)); + } + + @Test + public void testOPSLitWithMissingSigIsNotValid() { + PDA check = new PDA(); + check.next(InputSymbol.ONE_PASS_SIGNATURE); + check.next(InputSymbol.LITERAL_DATA); + assertThrows(MalformedOpenPgpMessageException.class, + () -> check.next(InputSymbol.END_OF_SEQUENCE)); + } + + @Test + public void testCompressedMessageWithStandalongAppendedSigIsNotValid() { + PDA check = new PDA(); + check.next(InputSymbol.COMPRESSED_DATA); + assertThrows(MalformedOpenPgpMessageException.class, + () -> check.next(InputSymbol.SIGNATURE)); + } + + @Test + public void testOPSCompressedDataWithMissingSigIsNotValid() { + PDA check = new PDA(); + check.next(InputSymbol.ONE_PASS_SIGNATURE); + check.next(InputSymbol.COMPRESSED_DATA); + assertThrows(MalformedOpenPgpMessageException.class, + () -> check.next(InputSymbol.END_OF_SEQUENCE)); + } + + @Test + public void testCompressedMessageFollowedByTrailingLiteralDataIsNotValid() { + PDA check = new PDA(); + check.next(InputSymbol.COMPRESSED_DATA); + assertThrows(MalformedOpenPgpMessageException.class, + () -> check.next(InputSymbol.LITERAL_DATA)); + } + + @Test + public void testOPSWithPrependedSigIsValid() { + PDA check = new PDA(); + check.next(InputSymbol.SIGNATURE); + check.next(InputSymbol.ONE_PASS_SIGNATURE); + check.next(InputSymbol.LITERAL_DATA); + check.next(InputSymbol.SIGNATURE); + check.next(InputSymbol.END_OF_SEQUENCE); + + assertTrue(check.isValid()); + } + + @Test + public void testPrependedSigInsideOPSSignedMessageIsValid() { + PDA check = new PDA(); + check.next(InputSymbol.ONE_PASS_SIGNATURE); + check.next(InputSymbol.SIGNATURE); + check.next(InputSymbol.LITERAL_DATA); + check.next(InputSymbol.SIGNATURE); + check.next(InputSymbol.END_OF_SEQUENCE); + + assertTrue(check.isValid()); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/AsciiArmorTest.java b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/AsciiArmorTest.java new file mode 100644 index 00000000..b6f8bf24 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/AsciiArmorTest.java @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.encryption_signing; + +import org.bouncycastle.openpgp.PGPException; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class AsciiArmorTest { + + @Test + public void testCustomAsciiArmorComments() throws PGPException, IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() + .onOutputStream(out) + .withOptions(ProducerOptions.noEncryptionNoSigning() + .setAsciiArmor(true) + .setComment("This is a comment.\nThis is another comment.")); + encryptionStream.write("Hello, World!".getBytes(StandardCharsets.UTF_8)); + encryptionStream.close(); + + String asciiArmored = out.toString(); + assertTrue(asciiArmored.contains("Comment: This is a comment.")); + assertTrue(asciiArmored.contains("Comment: This is another comment.")); + } + + @Test + public void testCustomAsciiArmorVersion() throws IOException, PGPException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() + .onOutputStream(out) + .withOptions(ProducerOptions.noEncryptionNoSigning() + .setAsciiArmor(true) + .setVersion("Custom-PGP 1.2.3")); + encryptionStream.write("Hello, World!".getBytes(StandardCharsets.UTF_8)); + encryptionStream.close(); + + String asciiArmored = out.toString(); + assertTrue(asciiArmored.contains("Version: Custom-PGP 1.2.3")); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/BcHashContextSignerTest.java b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/BcHashContextSignerTest.java new file mode 100644 index 00000000..1346e6b7 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/BcHashContextSignerTest.java @@ -0,0 +1,121 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.encryption_signing; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.InvalidAlgorithmParameterException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.HashAlgorithm; +import org.pgpainless.algorithm.SignatureType; +import org.pgpainless.decryption_verification.ConsumerOptions; +import org.pgpainless.decryption_verification.DecryptionStream; +import org.pgpainless.decryption_verification.MessageMetadata; +import org.pgpainless.key.generation.type.rsa.RsaLength; +import org.pgpainless.key.protection.SecretKeyRingProtector; + +public class BcHashContextSignerTest { + + private static final String message = "Hello, World!\n"; + private static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 62D5 CBED 8BD0 7D3F D167 240D 4364 E4C1 C4ED 8F59\n" + + "Comment: Sigfried \n" + + "\n" + + "lFgEYlnOkRYJKwYBBAHaRw8BAQdA7Kxn/sPIXo44xnxLBL81G5ghGzMikFc5ib9/\n" + + "qgJpZSUAAQCZnJN2l/cfWWh4DijBAwFWoVJOCphKhsJEjKxOzWdqMA2DtBVTaWdm\n" + + "cmllZCA8c2lnQGZyaS5lZD6IjwQTFgoAQQUCYlnOkQkQQ2TkwcTtj1kWIQRi1cvt\n" + + "i9B9P9FnJA1DZOTBxO2PWQKeAQKbAQUWAgMBAAQLCQgHBRUKCQgLApkBAAAd/gEA\n" + + "kiPFDdMGjZV/7Do/3ox46iCH3N1I3BGmA2Jt8PsYwe0BAKe5ahLzCNAXjBQU4iSD\n" + + "A4FGipNaG2ZWgAMkdwVjMLEAnF0EYlnOkRIKKwYBBAGXVQEFAQEHQI3n0cWbBh+7\n" + + "zeuBjMWevsyxLUCExKSj5fxCh/0GuJgAAwEIBwAA/16V22vjZfAvtnUrVtUZQoYZ\n" + + "E8h87Jzj/XxXFy63I6qoER2IdQQYFgoAHQUCYlnOkQKeAQKbDAUWAgMBAAQLCQgH\n" + + "BRUKCQgLAAoJEENk5MHE7Y9ZzhsA+gPb2FNutetjrYUSY7BEsk+PPkCXF9W6JZmb\n" + + "W/zyRxgpAP9zNzpWrO7kKQ0PwMMd3R1F4Rg6GH+vjXsIbd6jT25UBJxYBGJZzpEW\n" + + "CSsGAQQB2kcPAQEHQPOZhITstSj3cPfsTiBEPhtCrc184fkAjl4s+gSB9ttRAAD/\n" + + "RVpdc9BhJ/ZXtqQaCBL65h7Uym7i+HExQphHOiuB3iwQOIjVBBgWCgB9BQJiWc6R\n" + + "Ap4BApsCBRYCAwEABAsJCAcFFQoJCAtfIAQZFgoABgUCYlnOkQAKCRDXXcvYX8Ym\n" + + "crh9AP99WWietGWYs2//FYi0bEAWp6D0HmHP42rvC3qsqyMa8wD8D1Pi2atKwQTP\n" + + "JAxQFa06cUIw2POE3llaB0MKQXdTVgQACgkQQ2TkwcTtj1mF+gD+OHo68KeGFUi0\n" + + "VcVV/dx/6ES9GAIf1TI6jEsaU8TPBcMBAOHG+5MMVvyNiVKLA0JgJPF3JXOOEU+5\n" + + "qiHwlVoGncUM\n" + + "=431t\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + + @Test + public void signContextWithEdDSAKeys() throws PGPException, NoSuchAlgorithmException, IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); + signWithKeys(secretKeys); + } + + @Test + public void signContextWithRSAKeys() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().simpleRsaKeyRing("Sigfried", RsaLength._3072); + signWithKeys(secretKeys); + } + + @Test + public void signContextWithEcKeys() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().simpleEcKeyRing("Sigfried"); + signWithKeys(secretKeys); + } + + private void signWithKeys(PGPSecretKeyRing secretKeys) throws PGPException, NoSuchAlgorithmException, IOException { + for (HashAlgorithm hashAlgorithm : new HashAlgorithm[] { + HashAlgorithm.SHA256, HashAlgorithm.SHA384, HashAlgorithm.SHA512 + }) { + signFromContext(secretKeys, hashAlgorithm); + } + } + + private void signFromContext(PGPSecretKeyRing secretKeys, HashAlgorithm hashAlgorithm) + throws PGPException, NoSuchAlgorithmException, IOException { + PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKeys); + + byte[] messageBytes = message.getBytes(StandardCharsets.UTF_8); + ByteArrayInputStream messageIn = new ByteArrayInputStream(messageBytes); + + PGPSignature signature = signMessage(messageBytes, hashAlgorithm, secretKeys); + assertEquals(hashAlgorithm.getAlgorithmId(), signature.getHashAlgorithm()); + + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(messageIn) + .withOptions(new ConsumerOptions() + .addVerificationCert(certificate) + .addVerificationOfDetachedSignature(signature)); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Streams.pipeAll(decryptionStream, out); + decryptionStream.close(); + + MessageMetadata metadata = decryptionStream.getMetadata(); + assertTrue(metadata.isVerifiedSigned()); + } + + private PGPSignature signMessage(byte[] message, HashAlgorithm hashAlgorithm, PGPSecretKeyRing secretKeys) + throws NoSuchAlgorithmException, PGPException { + // Prepare the hash context + // This would be done by the caller application + MessageDigest messageDigest = MessageDigest.getInstance(hashAlgorithm.getAlgorithmName(), new BouncyCastleProvider()); + messageDigest.update(message); + + return BcHashContextSigner.signHashContext(messageDigest, SignatureType.BINARY_DOCUMENT, secretKeys, SecretKeyRingProtector.unprotectedKeys()); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptDecryptTest.java b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptDecryptTest.java index 38dda178..3e620386 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptDecryptTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptDecryptTest.java @@ -25,17 +25,16 @@ import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.DocumentSignatureType; import org.pgpainless.algorithm.KeyFlag; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.decryption_verification.ConsumerOptions; import org.pgpainless.decryption_verification.DecryptionStream; -import org.pgpainless.decryption_verification.OpenPgpMetadata; -import org.pgpainless.implementation.ImplementationFactory; +import org.pgpainless.decryption_verification.MessageMetadata; +import org.pgpainless.exception.KeyException; import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.key.TestKeys; import org.pgpainless.key.generation.KeySpec; @@ -48,10 +47,12 @@ import org.pgpainless.key.protection.UnprotectedKeysProtector; import org.pgpainless.key.util.KeyRingUtils; import org.pgpainless.policy.Policy; import org.pgpainless.util.ArmoredOutputStreamFactory; +import org.pgpainless.util.TestAllImplementations; public class EncryptDecryptTest { // Don't use StandardCharsets.UTF_8 because of Android API level. + @SuppressWarnings("CharsetObjectCanBeUsed") private static final Charset UTF8 = Charset.forName("UTF-8"); private static final String testMessage = @@ -65,18 +66,17 @@ public class EncryptDecryptTest { @BeforeEach public void setDefaultPolicy() { PGPainless.getPolicy().setSymmetricKeyEncryptionAlgorithmPolicy( - Policy.SymmetricKeyAlgorithmPolicy.defaultSymmetricKeyEncryptionAlgorithmPolicy()); + Policy.SymmetricKeyAlgorithmPolicy.symmetricKeyEncryptionPolicy2022()); PGPainless.getPolicy().setSymmetricKeyDecryptionAlgorithmPolicy( - Policy.SymmetricKeyAlgorithmPolicy.defaultSymmetricKeyDecryptionAlgorithmPolicy()); + Policy.SymmetricKeyAlgorithmPolicy.symmetricKeyDecryptionPolicy2022()); } - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void freshKeysRsaToElGamalTest(ImplementationFactory implementationFactory) + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void freshKeysRsaToElGamalTest() throws PGPException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, IOException { - ImplementationFactory.setFactoryImplementation(implementationFactory); PGPSecretKeyRing sender = PGPainless.generateKeyRing().simpleRsaKeyRing("romeo@montague.lit", RsaLength._3072); - PGPSecretKeyRing recipient = PGPainless.generateKeyRing() + PGPSecretKeyRing recipient = PGPainless.buildKeyRing() .setPrimaryKey(KeySpec.getBuilder( KeyType.RSA(RsaLength._4096), KeyFlag.SIGN_DATA, KeyFlag.CERTIFY_OTHER)) @@ -88,54 +88,49 @@ public class EncryptDecryptTest { encryptDecryptForSecretKeyRings(sender, recipient); } - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void freshKeysRsaToRsaTest(ImplementationFactory implementationFactory) + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void freshKeysRsaToRsaTest() throws PGPException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, IOException { - ImplementationFactory.setFactoryImplementation(implementationFactory); PGPSecretKeyRing sender = PGPainless.generateKeyRing().simpleRsaKeyRing("romeo@montague.lit", RsaLength._3072); PGPSecretKeyRing recipient = PGPainless.generateKeyRing().simpleRsaKeyRing("juliet@capulet.lit", RsaLength._3072); encryptDecryptForSecretKeyRings(sender, recipient); } - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void freshKeysEcToEcTest(ImplementationFactory implementationFactory) + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void freshKeysEcToEcTest() throws IOException, PGPException, NoSuchAlgorithmException, InvalidAlgorithmParameterException { - ImplementationFactory.setFactoryImplementation(implementationFactory); PGPSecretKeyRing sender = PGPainless.generateKeyRing().simpleEcKeyRing("romeo@montague.lit"); PGPSecretKeyRing recipient = PGPainless.generateKeyRing().simpleEcKeyRing("juliet@capulet.lit"); encryptDecryptForSecretKeyRings(sender, recipient); } - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void freshKeysEcToRsaTest(ImplementationFactory implementationFactory) + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void freshKeysEcToRsaTest() throws PGPException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, IOException { - ImplementationFactory.setFactoryImplementation(implementationFactory); PGPSecretKeyRing sender = PGPainless.generateKeyRing().simpleEcKeyRing("romeo@montague.lit"); PGPSecretKeyRing recipient = PGPainless.generateKeyRing().simpleRsaKeyRing("juliet@capulet.lit", RsaLength._3072); encryptDecryptForSecretKeyRings(sender, recipient); } - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void freshKeysRsaToEcTest(ImplementationFactory implementationFactory) + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void freshKeysRsaToEcTest() throws PGPException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, IOException { - ImplementationFactory.setFactoryImplementation(implementationFactory); PGPSecretKeyRing sender = PGPainless.generateKeyRing().simpleRsaKeyRing("romeo@montague.lit", RsaLength._3072); PGPSecretKeyRing recipient = PGPainless.generateKeyRing().simpleEcKeyRing("juliet@capulet.lit"); encryptDecryptForSecretKeyRings(sender, recipient); } - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void existingRsaKeysTest(ImplementationFactory implementationFactory) throws IOException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void existingRsaKeysTest() throws IOException, PGPException { PGPSecretKeyRing sender = TestKeys.getJulietSecretKeyRing(); PGPSecretKeyRing recipient = TestKeys.getRomeoSecretKeyRing(); @@ -190,17 +185,15 @@ public class EncryptDecryptTest { decryptor.close(); assertArrayEquals(secretMessage, decryptedSecretMessage.toByteArray()); - OpenPgpMetadata result = decryptor.getResult(); - assertTrue(result.containsVerifiedSignatureFrom(senderPub)); - assertTrue(result.isSigned()); + MessageMetadata result = decryptor.getMetadata(); + assertTrue(result.isVerifiedSignedBy(senderPub)); assertTrue(result.isEncrypted()); - assertTrue(result.isVerified()); + assertTrue(result.isVerifiedSigned()); } - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void testDetachedSignatureCreationAndVerification(ImplementationFactory implementationFactory) throws IOException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void testDetachedSignatureCreationAndVerification() throws IOException, PGPException { PGPSecretKeyRing signingKeys = TestKeys.getJulietSecretKeyRing(); SecretKeyRingProtector keyRingProtector = new UnprotectedKeysProtector(); @@ -239,14 +232,13 @@ public class EncryptDecryptTest { Streams.pipeAll(verifier, dummyOut); verifier.close(); - OpenPgpMetadata decryptionResult = verifier.getResult(); + MessageMetadata decryptionResult = verifier.getMetadata(); assertFalse(decryptionResult.getVerifiedSignatures().isEmpty()); } - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void testOnePassSignatureCreationAndVerification(ImplementationFactory implementationFactory) throws IOException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void testOnePassSignatureCreationAndVerification() throws IOException, PGPException { PGPSecretKeyRing signingKeys = TestKeys.getJulietSecretKeyRing(); SecretKeyRingProtector keyRingProtector = new UnprotectedKeysProtector(); byte[] data = testMessage.getBytes(); @@ -270,11 +262,12 @@ public class EncryptDecryptTest { Streams.pipeAll(verifier, signOut); verifier.close(); - OpenPgpMetadata metadata = verifier.getResult(); + MessageMetadata metadata = verifier.getMetadata(); assertFalse(metadata.getVerifiedSignatures().isEmpty()); } - @Test + @TestTemplate + @ExtendWith(TestAllImplementations.class) public void expiredSubkeyBacksigTest() throws IOException { String key = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + "\n" + @@ -333,7 +326,7 @@ public class EncryptDecryptTest { PGPPublicKeyRing publicKeys = PGPainless.readKeyRing().publicKeyRing(key); - assertThrows(IllegalArgumentException.class, () -> + assertThrows(KeyException.UnacceptableEncryptionKeyException.class, () -> EncryptionOptions.encryptCommunications() .addRecipient(publicKeys)); } diff --git a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionOptionsTest.java b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionOptionsTest.java index 78d9057d..5f6c6c15 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionOptionsTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionOptionsTest.java @@ -9,9 +9,9 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import java.io.IOException; import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Iterator; @@ -23,20 +23,23 @@ import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.KeyFlag; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; -import org.pgpainless.exception.KeyValidationError; +import org.pgpainless.exception.KeyException; import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.key.generation.KeySpec; import org.pgpainless.key.generation.type.KeyType; -import org.pgpainless.key.generation.type.eddsa.EdDSACurve; -import org.pgpainless.key.generation.type.xdh.XDHSpec; +import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve; +import org.pgpainless.key.generation.type.xdh_legacy.XDHLegacySpec; import org.pgpainless.key.util.KeyRingUtils; import org.pgpainless.util.Passphrase; +import javax.annotation.Nonnull; + public class EncryptionOptionsTest { private static PGPSecretKeyRing secretKeys; @@ -47,12 +50,12 @@ public class EncryptionOptionsTest { @BeforeAll public static void generateKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - secretKeys = PGPainless.generateKeyRing() - .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER) + secretKeys = PGPainless.buildKeyRing() + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER) .build()) - .addSubkey(KeySpec.getBuilder(KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_COMMS) + .addSubkey(KeySpec.getBuilder(KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.ENCRYPT_COMMS) .build()) - .addSubkey(KeySpec.getBuilder(KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_STORAGE) + .addSubkey(KeySpec.getBuilder(KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.ENCRYPT_STORAGE) .build()) .addUserId("test@pgpainless.org") .build(); @@ -116,50 +119,59 @@ public class EncryptionOptionsTest { assertTrue(encryptionKeys.contains(encryptStorage)); } + @Test + public void testAddEmptyRecipientsFails() { + EncryptionOptions options = new EncryptionOptions(); + assertThrows(IllegalArgumentException.class, () -> options.addRecipients(Collections.emptyList())); + assertThrows(IllegalArgumentException.class, () -> options.addRecipients(Collections.emptyList(), + ArrayList::new)); + } + @Test public void testAddEmptyPassphraseFails() { EncryptionOptions options = new EncryptionOptions(); assertThrows(IllegalArgumentException.class, () -> - options.addPassphrase(Passphrase.emptyPassphrase())); + options.addMessagePassphrase(Passphrase.emptyPassphrase())); } @Test public void testAddRecipient_KeyWithoutEncryptionKeyFails() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { EncryptionOptions options = new EncryptionOptions(); - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() - .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA)) + PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA)) .addUserId("test@pgpainless.org") .build(); PGPPublicKeyRing publicKeys = KeyRingUtils.publicKeyRingFrom(secretKeys); - assertThrows(IllegalArgumentException.class, () -> options.addRecipient(publicKeys)); + assertThrows(KeyException.UnacceptableEncryptionKeyException.class, () -> options.addRecipient(publicKeys)); } @Test public void testEncryptionKeySelectionStrategyEmpty_ThrowsAssertionError() { EncryptionOptions options = new EncryptionOptions(); - assertThrows(IllegalArgumentException.class, + assertThrows(KeyException.UnacceptableEncryptionKeyException.class, () -> options.addRecipient(publicKeys, new EncryptionOptions.EncryptionKeySelector() { + @NotNull @Override - public List selectEncryptionSubkeys(List encryptionCapableKeys) { + public List selectEncryptionSubkeys(@NotNull List encryptionCapableKeys) { return Collections.emptyList(); } })); - assertThrows(IllegalArgumentException.class, + assertThrows(KeyException.UnacceptableEncryptionKeyException.class, () -> options.addRecipient(publicKeys, "test@pgpainless.org", new EncryptionOptions.EncryptionKeySelector() { @Override - public List selectEncryptionSubkeys(List encryptionCapableKeys) { + public List selectEncryptionSubkeys(@Nonnull List encryptionCapableKeys) { return Collections.emptyList(); } })); } @Test - public void testAddRecipients_PGPPublicKeyRingCollection() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + public void testAddRecipients_PGPPublicKeyRingCollection() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { PGPPublicKeyRing secondKeyRing = KeyRingUtils.publicKeyRingFrom( - PGPainless.generateKeyRing().modernKeyRing("other@pgpainless.org", null)); + PGPainless.generateKeyRing().modernKeyRing("other@pgpainless.org")); PGPPublicKeyRingCollection collection = new PGPPublicKeyRingCollection( Arrays.asList(publicKeys, secondKeyRing)); @@ -180,6 +192,6 @@ public class EncryptionOptionsTest { @Test public void testAddRecipient_withInvalidUserId() { EncryptionOptions options = new EncryptionOptions(); - assertThrows(KeyValidationError.class, () -> options.addRecipient(publicKeys, "invalid@user.id")); + assertThrows(KeyException.UnboundUserIdException.class, () -> options.addRecipient(publicKeys, "invalid@user.id")); } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionStreamClosedTest.java b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionStreamClosedTest.java index c667de31..ae640661 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionStreamClosedTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionStreamClosedTest.java @@ -11,23 +11,22 @@ import java.io.IOException; import java.io.OutputStream; import org.bouncycastle.openpgp.PGPException; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; import org.pgpainless.PGPainless; -import org.pgpainless.implementation.ImplementationFactory; +import org.pgpainless.util.TestAllImplementations; import org.pgpainless.util.Passphrase; public class EncryptionStreamClosedTest { - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void testStreamHasToBeClosedBeforeGetResultCanBeCalled(ImplementationFactory implementationFactory) throws IOException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void testStreamHasToBeClosedBeforeGetResultCanBeCalled() throws IOException, PGPException { OutputStream out = new ByteArrayOutputStream(); EncryptionStream stream = PGPainless.encryptAndOrSign() .onOutputStream(out) .withOptions(ProducerOptions.encrypt(EncryptionOptions.encryptCommunications() - .addPassphrase(Passphrase.fromPassword("dummy")))); + .addMessagePassphrase(Passphrase.fromPassword("dummy")))); // No close() called => getResult throws assertThrows(IllegalStateException.class, stream::getResult); diff --git a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionWithMissingKeyFlagsTest.java b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionWithMissingKeyFlagsTest.java new file mode 100644 index 00000000..5fd4a674 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionWithMissingKeyFlagsTest.java @@ -0,0 +1,194 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.encryption_signing; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Date; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.decryption_verification.ConsumerOptions; +import org.pgpainless.decryption_verification.DecryptionStream; +import org.pgpainless.decryption_verification.MessageMetadata; +import org.pgpainless.exception.KeyException; +import org.pgpainless.util.DateUtil; + +public class EncryptionWithMissingKeyFlagsTest { + + private static final String KEY_WITHOUT_KEY_FLAGS = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: A991 A732 9021 7D2D 1250 93C3 2503 6033 1D81 E264\n" + + "Comment: Test \n" + + "\n" + + "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\n" + + "lh0k0LBQrX/YNegabaT+3gffzz7/\n" + + "=TKhx\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + + // Above key expires in 5 years, so we fix the evaluation date to a known-good value + private static final Date evaluationDate = DateUtil.parseUTCDate("2023-08-03 12:50:06 UTC"); + private static final String MESSAGE = "Hello, World!\n"; + + @Test + public void testEncryptionForKeyWithoutKeyFlagsFailsByDefault() + throws IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing() + .secretKeyRing(KEY_WITHOUT_KEY_FLAGS); + PGPPublicKeyRing publicKeys = PGPainless.extractCertificate(secretKeys); + + assertThrows(KeyException.UnacceptableEncryptionKeyException.class, () -> + EncryptionOptions.get() + .setEvaluationDate(evaluationDate) + .addRecipient(publicKeys)); + } + + + @Test + public void testEncryptionForKeyWithoutKeyFlagsSucceedsWithActiveWorkaround() + throws PGPException, IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing() + .secretKeyRing(KEY_WITHOUT_KEY_FLAGS); + PGPPublicKeyRing publicKeys = PGPainless.extractCertificate(secretKeys); + + // Prepare encryption + ByteArrayOutputStream out = new ByteArrayOutputStream(); + EncryptionStream encOut = PGPainless.encryptAndOrSign() + .onOutputStream(out) + .withOptions(ProducerOptions.encrypt(EncryptionOptions.get() + .setEvaluationDate(evaluationDate) + .setAllowEncryptionWithMissingKeyFlags() // Workaround + .addRecipient(publicKeys))); + + // Encrypt + encOut.write(MESSAGE.getBytes(StandardCharsets.UTF_8)); + encOut.close(); + + // Prepare decryption + ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(in) + .withOptions(ConsumerOptions.get().addDecryptionKey(secretKeys)); + ByteArrayOutputStream plain = new ByteArrayOutputStream(); + + // Decrypt + Streams.pipeAll(decryptionStream, plain); + decryptionStream.close(); + + // Check result + assertEquals(MESSAGE, plain.toString()); + MessageMetadata metadata = decryptionStream.getMetadata(); + assertTrue(metadata.isEncryptedFor(publicKeys)); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/FileInformationTest.java b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/FileInformationTest.java index c5a4d28c..50e9722b 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/FileInformationTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/FileInformationTest.java @@ -6,6 +6,7 @@ package org.pgpainless.encryption_signing; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.ByteArrayInputStream; @@ -28,7 +29,7 @@ import org.pgpainless.PGPainless; import org.pgpainless.algorithm.StreamEncoding; import org.pgpainless.decryption_verification.ConsumerOptions; import org.pgpainless.decryption_verification.DecryptionStream; -import org.pgpainless.decryption_verification.OpenPgpMetadata; +import org.pgpainless.decryption_verification.MessageMetadata; public class FileInformationTest { @@ -38,7 +39,7 @@ public class FileInformationTest { @BeforeAll public static void generateKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - secretKey = PGPainless.generateKeyRing().modernKeyRing("alice@wonderland.lit", null); + secretKey = PGPainless.generateKeyRing().modernKeyRing("alice@wonderland.lit"); certificate = PGPainless.extractCertificate(secretKey); } @@ -80,11 +81,11 @@ public class FileInformationTest { decryptionStream.close(); - OpenPgpMetadata decResult = decryptionStream.getResult(); + MessageMetadata decResult = decryptionStream.getMetadata(); - assertEquals(fileName, decResult.getFileName()); + assertEquals(fileName, decResult.getFilename()); JUtils.assertDateEquals(modificationDate, decResult.getModificationDate()); - assertEquals(encoding, decResult.getFileEncoding()); + assertEquals(encoding, decResult.getLiteralDataEncoding()); } @Test @@ -119,11 +120,12 @@ public class FileInformationTest { decryptionStream.close(); - OpenPgpMetadata decResult = decryptionStream.getResult(); + MessageMetadata decResult = decryptionStream.getMetadata(); - assertEquals("", decResult.getFileName()); + assertEquals("", decResult.getFilename()); JUtils.assertDateEquals(PGPLiteralData.NOW, decResult.getModificationDate()); - assertEquals(PGPLiteralData.BINARY, decResult.getFileEncoding().getCode()); + assertNotNull(decResult.getLiteralDataEncoding()); + assertEquals(PGPLiteralData.BINARY, decResult.getLiteralDataEncoding().getCode()); assertFalse(decResult.isForYourEyesOnly()); } @@ -160,11 +162,12 @@ public class FileInformationTest { decryptionStream.close(); - OpenPgpMetadata decResult = decryptionStream.getResult(); + MessageMetadata decResult = decryptionStream.getMetadata(); - assertEquals(PGPLiteralData.CONSOLE, decResult.getFileName()); + assertEquals(PGPLiteralData.CONSOLE, decResult.getFilename()); JUtils.assertDateEquals(PGPLiteralData.NOW, decResult.getModificationDate()); - assertEquals(PGPLiteralData.BINARY, decResult.getFileEncoding().getCode()); + assertNotNull(decResult.getLiteralDataEncoding()); + assertEquals(PGPLiteralData.BINARY, decResult.getLiteralDataEncoding().getCode()); assertTrue(decResult.isForYourEyesOnly()); } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/HiddenRecipientEncryptionTest.java b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/HiddenRecipientEncryptionTest.java new file mode 100644 index 00000000..1cb0d3cb --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/HiddenRecipientEncryptionTest.java @@ -0,0 +1,71 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.encryption_signing; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.decryption_verification.ConsumerOptions; +import org.pgpainless.decryption_verification.DecryptionStream; +import org.pgpainless.decryption_verification.MessageMetadata; +import org.pgpainless.key.SubkeyIdentifier; + +/** + * Test encryption with anonymous recipients. + */ +public class HiddenRecipientEncryptionTest { + + @Test + public void testAnonymousRecipientRoundtrip() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() + .modernKeyRing("Alice "); + PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKeys); + + String msg = "Hello, World!\n"; + + ByteArrayOutputStream ciphertextOut = new ByteArrayOutputStream(); + EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() + .onOutputStream(ciphertextOut) + .withOptions(ProducerOptions.encrypt( + EncryptionOptions.get() + .addHiddenRecipient(certificate) + )); + encryptionStream.write(msg.getBytes(StandardCharsets.UTF_8)); + encryptionStream.close(); + EncryptionResult result = encryptionStream.getResult(); + SubkeyIdentifier actualEncryptionKey = result.getRecipients().iterator().next(); + + byte[] ciphertext = ciphertextOut.toByteArray(); + + ByteArrayInputStream ciphertextIn = new ByteArrayInputStream(ciphertext); + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(ciphertextIn) + .withOptions(ConsumerOptions.get() + .addDecryptionKey(secretKeys)); + + ByteArrayOutputStream plaintextOut = new ByteArrayOutputStream(); + Streams.pipeAll(decryptionStream, plaintextOut); + + decryptionStream.close(); + MessageMetadata metadata = decryptionStream.getMetadata(); + + assertEquals(msg, plaintextOut.toString()); + assertTrue(metadata.getRecipientKeyIds().contains(0L)); + assertEquals(actualEncryptionKey, metadata.getDecryptionKey()); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/HideArmorHeadersTest.java b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/HideArmorHeadersTest.java new file mode 100644 index 00000000..59ad4596 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/HideArmorHeadersTest.java @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.encryption_signing; + +import org.bouncycastle.openpgp.PGPException; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.util.Passphrase; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class HideArmorHeadersTest { + + @Test + public void testVersionHeaderIsOmitted() throws PGPException, IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() + .onOutputStream(out) + .withOptions(ProducerOptions.encrypt( + EncryptionOptions.get() + .addMessagePassphrase(Passphrase.fromPassword("sw0rdf1sh"))) + .setHideArmorHeaders(true)); + + encryptionStream.write("Hello, World!\n".getBytes()); + encryptionStream.close(); + + assertTrue(out.toString().startsWith("-----BEGIN PGP MESSAGE-----\n\n")); // No "Version: PGPainless" + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/MultiSigningSubkeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/MultiSigningSubkeyTest.java new file mode 100644 index 00000000..28097db6 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/MultiSigningSubkeyTest.java @@ -0,0 +1,145 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.encryption_signing; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.DocumentSignatureType; +import org.pgpainless.algorithm.KeyFlag; +import org.pgpainless.decryption_verification.ConsumerOptions; +import org.pgpainless.decryption_verification.DecryptionStream; +import org.pgpainless.decryption_verification.SignatureVerification; +import org.pgpainless.key.SubkeyIdentifier; +import org.pgpainless.key.generation.KeySpec; +import org.pgpainless.key.generation.type.KeyType; +import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve; +import org.pgpainless.key.generation.type.rsa.RsaLength; +import org.pgpainless.key.generation.type.xdh_legacy.XDHLegacySpec; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.util.MultiMap; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.util.Iterator; +import java.util.List; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class MultiSigningSubkeyTest { + + private static PGPSecretKeyRing signingKey; + private static PGPPublicKeyRing signingCert; + private static SubkeyIdentifier primaryKey; + private static SubkeyIdentifier signingKey1; + private static SubkeyIdentifier signingKey2; + private static SecretKeyRingProtector protector; + + @BeforeAll + public static void generateKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + signingKey = PGPainless.buildKeyRing() + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA)) + .addSubkey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.SIGN_DATA)) + .addSubkey(KeySpec.getBuilder(KeyType.RSA(RsaLength._3072), KeyFlag.SIGN_DATA)) + .addSubkey(KeySpec.getBuilder(KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)) + .addUserId("Alice ") + .build(); + signingCert = PGPainless.extractCertificate(signingKey); + Iterator signingSubkeys = PGPainless.inspectKeyRing(signingKey).getSigningSubkeys().listIterator(); + primaryKey = new SubkeyIdentifier(signingKey, signingSubkeys.next().getKeyID()); + signingKey1 = new SubkeyIdentifier(signingKey, signingSubkeys.next().getKeyID()); + signingKey2 = new SubkeyIdentifier(signingKey, signingSubkeys.next().getKeyID()); + protector = SecretKeyRingProtector.unprotectedKeys(); + } + + @Test + public void detachedSignWithAllSubkeys() throws PGPException, IOException { + ByteArrayInputStream dataIn = new ByteArrayInputStream("Hello, World!\n".getBytes(StandardCharsets.UTF_8)); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + EncryptionStream signingStream = PGPainless.encryptAndOrSign() + .onOutputStream(out) + .withOptions(ProducerOptions.sign(SigningOptions.get().addDetachedSignature(protector, signingKey, DocumentSignatureType.BINARY_DOCUMENT))); + Streams.pipeAll(dataIn, signingStream); + signingStream.close(); + + MultiMap sigs = signingStream.getResult().getDetachedSignatures(); + assertTrue(sigs.containsKey(primaryKey)); + assertTrue(sigs.containsKey(signingKey1)); + assertTrue(sigs.containsKey(signingKey2)); + } + + @Test + public void detachedSignWithSingleSubkey() throws PGPException, IOException { + ByteArrayInputStream dataIn = new ByteArrayInputStream("Hello, World!\n".getBytes(StandardCharsets.UTF_8)); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + EncryptionStream signingStream = PGPainless.encryptAndOrSign() + .onOutputStream(out) + .withOptions(ProducerOptions.sign(SigningOptions.get().addDetachedSignature(protector, signingKey, signingKey1.getKeyId()))); + Streams.pipeAll(dataIn, signingStream); + signingStream.close(); + + MultiMap sigs = signingStream.getResult().getDetachedSignatures(); + assertEquals(1, sigs.flatten().size()); + assertTrue(sigs.containsKey(signingKey1)); + } + + @Test + public void inlineSignWithAllSubkeys() throws PGPException, IOException { + ByteArrayInputStream dataIn = new ByteArrayInputStream("Hello, World!\n".getBytes(StandardCharsets.UTF_8)); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + EncryptionStream signingStream = PGPainless.encryptAndOrSign() + .onOutputStream(out) + .withOptions(ProducerOptions.sign(SigningOptions.get().addInlineSignature(protector, signingKey, DocumentSignatureType.BINARY_DOCUMENT))); + Streams.pipeAll(dataIn, signingStream); + signingStream.close(); + + ByteArrayInputStream signedIn = new ByteArrayInputStream(out.toByteArray()); + DecryptionStream verificationStream = PGPainless.decryptAndOrVerify().onInputStream(signedIn) + .withOptions(ConsumerOptions.get().addVerificationCert(signingCert)); + Streams.drain(verificationStream); + verificationStream.close(); + + List sigs = verificationStream.getMetadata().getVerifiedSignatures(); + List sigKeys = sigs.stream().map(SignatureVerification::getSigningKey) + .collect(Collectors.toList()); + assertTrue(sigKeys.contains(primaryKey)); + assertTrue(sigKeys.contains(signingKey1)); + assertTrue(sigKeys.contains(signingKey2)); + } + + @Test + public void inlineSignWithSingleSubkey() throws PGPException, IOException { + ByteArrayInputStream dataIn = new ByteArrayInputStream("Hello, World!\n".getBytes(StandardCharsets.UTF_8)); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + EncryptionStream signingStream = PGPainless.encryptAndOrSign() + .onOutputStream(out) + .withOptions(ProducerOptions.sign(SigningOptions.get().addInlineSignature(protector, signingKey, signingKey1.getKeyId()))); + Streams.pipeAll(dataIn, signingStream); + signingStream.close(); + + ByteArrayInputStream signedIn = new ByteArrayInputStream(out.toByteArray()); + DecryptionStream verificationStream = PGPainless.decryptAndOrVerify().onInputStream(signedIn) + .withOptions(ConsumerOptions.get().addVerificationCert(signingCert)); + Streams.drain(verificationStream); + verificationStream.close(); + + List sigs = verificationStream.getMetadata().getVerifiedSignatures(); + assertEquals(1, sigs.size()); + assertEquals(signingKey1, sigs.get(0).getSigningKey()); + } + +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/RespectPreferredSymmetricAlgorithmDuringEncryptionTest.java b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/RespectPreferredSymmetricAlgorithmDuringEncryptionTest.java index f866e962..55564d84 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/RespectPreferredSymmetricAlgorithmDuringEncryptionTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/RespectPreferredSymmetricAlgorithmDuringEncryptionTest.java @@ -18,68 +18,69 @@ import org.pgpainless.algorithm.SymmetricKeyAlgorithm; public class RespectPreferredSymmetricAlgorithmDuringEncryptionTest { @Test - public void onlyAES128() throws IOException, PGPException { - // Key has [AES128] as preferred symm. algo on latest user-id cert - String key = "-----BEGIN PGP ARMORED FILE-----\n" + - "Comment: ASCII Armor added by openpgp-interoperability-test-suite\n" + + public void algorithmPreferencesAreRespectedDependingOnEncryptionTarget() throws IOException, PGPException { + // Key has AES256, AES192, AES128 as primary user-ids sym algo prefs, + // and AES128 as secondary user-id prefs + String key = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 7E13 2E9C EAE8 7E7B AD6C 5329 94CE B847 EEFB 044B\n" + + "Comment: Bob Babbage \n" + "\n" + - "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\n" + - "5KjhX2PVNEJd3XZRzaXZE2aAMQ==\n" + - "=d5ke\n" + - "-----END PGP ARMORED FILE-----\n"; + "mDMEYeIhnhYJKwYBBAHaRw8BAQdAfs9SkOSEyAQmvwLwwUPCp3Qiw2t4rm+e7n8t\n" + + "oVjAmle0IUJvYiBCYWJiYWdlIDxib2JAb3BlbnBncC5leGFtcGxlPoiPBBMWCgBB\n" + + "BQJh4iGeCZCUzrhH7vsESxahBH4TLpzq6H57rWxTKZTOuEfu+wRLAp4BApsBBZYC\n" + + "AwEABIsJCAcFlQoJCAsCmQEAAKK/AP4lCifuXpZIUR4PrenGBZFtoZpB5s1i/YrB\n" + + "cnCuodQX9wEAyENhlXNYopWdgBZ9g4E1Y0cJfpwCwWhx0DeATmrSzAO0H0JvYmJ5\n" + + "MTI4IDxib2JieUBhZXMxMjguZXhhbXBsZT6IigQTFgoAPAUCYeIhngmQlM64R+77\n" + + "BEsWoQR+Ey6c6uh+e61sUymUzrhH7vsESwKeAQKbAQWWAgMBAAKLBwWVCgkICwAA\n" + + "y0wBAIhAEpQgJRizHitPx3WUpIYbKq3R5jAO34NnlmTzNVj6AP9aWHPsW5r7HuQh\n" + + "xJz+8zdCOuAxKv6tvHthSWJ64VWDDrg4BGHiIZ4SCisGAQQBl1UBBQEBB0CEIv13\n" + + "/qTXR0wiUG5DVZCWh/KLKrF5TemUfYXA/kBTOAMBCAeIdQQYFgoAHQUCYeIhngKe\n" + + "AQKbDAWWAgMBAASLCQgHBZUKCQgLAAoJEJTOuEfu+wRLwC4A/0/VDPPDE6kT/8C3\n" + + "9d8ekZkQE38o2nC58E62AjM5O2x6AQDMd0gcoKIxPi9uRi3nVsNS233a3MxFEjpe\n" + + "qqgyBnqxBLgzBGHiIZ4WCSsGAQQB2kcPAQEHQP7IGdT9moutwtys4A/ndkWJVWn/\n" + + "zkoOn3cSad1bP8y8iNUEGBYKAH0FAmHiIZ4CngECmwIFlgIDAQAEiwkIBwWVCgkI\n" + + "C18gBBkWCgAGBQJh4iGeAAoJENcuZc0+RPVgrucBAI+IzpplBIpySOIyzHJdjeFt\n" + + "ikwTBOY3OTriY2Z62Ec6AQDhVxO7LZuH3mTCklj4HelfMrhlqUlnYr7qCIjzI5BY\n" + + "BwAKCRCUzrhH7vsES4snAP4qzlEbaHpN7ZPomCOHD7J2+CHlyTtsRP45XWVCqNH1\n" + + "jAEAmzz5Lu67k97AzArpoGHgYh492w5BfdApV8BCaTW4AgI=\n" + + "=XwJQ\n" + + "-----END PGP PUBLIC KEY BLOCK-----\n"; PGPPublicKeyRing publicKeys = PGPainless.readKeyRing().publicKeyRing(key); + + + // Encrypt without specifying user-id + // PGPainless now inspects the primary user-ids signature to get sym alg prefs (AES256, AES192, AES128) ByteArrayOutputStream out = new ByteArrayOutputStream(); EncryptionStream encryptionStream = PGPainless.encryptAndOrSign().onOutputStream(out) .withOptions( ProducerOptions.encrypt(new EncryptionOptions() - .addRecipient(publicKeys) + .addRecipient(publicKeys) // no user-id passed + )); + + encryptionStream.close(); + assertEquals(SymmetricKeyAlgorithm.AES_256, encryptionStream.getResult().getEncryptionAlgorithm()); + + // Encrypt to the primary user-id + // PGPainless should extract algorithm preferences from the latest user-id sig in this case (AES-256, AES-192, AES-128) + out = new ByteArrayOutputStream(); + encryptionStream = PGPainless.encryptAndOrSign().onOutputStream(out) + .withOptions( + ProducerOptions.encrypt(new EncryptionOptions() + .addRecipient(publicKeys, "Bob Babbage ") + )); + + encryptionStream.close(); + assertEquals(SymmetricKeyAlgorithm.AES_256, encryptionStream.getResult().getEncryptionAlgorithm()); + + // Encrypt to the secondary user-id + // PGPainless extracts algorithm preferences from secondary user-id sig, in this case AES-128 + out = new ByteArrayOutputStream(); + encryptionStream = PGPainless.encryptAndOrSign().onOutputStream(out) + .withOptions( + ProducerOptions.encrypt(new EncryptionOptions() + .addRecipient(publicKeys, "Bobby128 ") )); encryptionStream.close(); diff --git a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/SigningTest.java b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/SigningTest.java index a8663ba9..c62116b3 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/SigningTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/SigningTest.java @@ -26,36 +26,34 @@ import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.util.io.Streams; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.DocumentSignatureType; import org.pgpainless.algorithm.HashAlgorithm; import org.pgpainless.algorithm.KeyFlag; import org.pgpainless.decryption_verification.ConsumerOptions; import org.pgpainless.decryption_verification.DecryptionStream; -import org.pgpainless.decryption_verification.OpenPgpMetadata; -import org.pgpainless.exception.KeyCannotSignException; -import org.pgpainless.exception.KeyValidationError; -import org.pgpainless.implementation.ImplementationFactory; +import org.pgpainless.decryption_verification.MessageMetadata; +import org.pgpainless.exception.KeyException; import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.key.TestKeys; import org.pgpainless.key.generation.KeySpec; import org.pgpainless.key.generation.type.KeyType; -import org.pgpainless.key.generation.type.eddsa.EdDSACurve; +import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve; import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.util.KeyRingUtils; import org.pgpainless.util.MultiMap; import org.pgpainless.util.Passphrase; +import org.pgpainless.util.TestAllImplementations; public class SigningTest { - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void testEncryptionAndSignatureVerification(ImplementationFactory implementationFactory) throws IOException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void testEncryptionAndSignatureVerification() + throws IOException, PGPException { PGPPublicKeyRing julietKeys = TestKeys.getJulietPublicKeyRing(); PGPPublicKeyRing romeoKeys = TestKeys.getRomeoPublicKeyRing(); @@ -73,12 +71,13 @@ public class SigningTest { EncryptionOptions.encryptDataAtRest() .addRecipients(keys) .addRecipient(KeyRingUtils.publicKeyRingFrom(cryptieKeys)), - new SigningOptions() - .addInlineSignature(SecretKeyRingProtector.unlockSingleKeyWith(TestKeys.CRYPTIE_PASSPHRASE, cryptieSigningKey), + new SigningOptions().addInlineSignature( + SecretKeyRingProtector.unlockSingleKeyWith(TestKeys.CRYPTIE_PASSPHRASE, cryptieSigningKey), cryptieKeys, TestKeys.CRYPTIE_UID, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT) ).setAsciiArmor(true)); - byte[] messageBytes = "This message is signed and encrypted to Romeo and Juliet.".getBytes(StandardCharsets.UTF_8); + byte[] messageBytes = "This message is signed and encrypted to Romeo and Juliet." + .getBytes(StandardCharsets.UTF_8); ByteArrayInputStream message = new ByteArrayInputStream(messageBytes); Streams.pipeAll(message, encryptionStream); @@ -90,8 +89,10 @@ public class SigningTest { PGPSecretKeyRing romeoSecret = TestKeys.getRomeoSecretKeyRing(); PGPSecretKeyRing julietSecret = TestKeys.getJulietSecretKeyRing(); - PGPSecretKeyRingCollection secretKeys = new PGPSecretKeyRingCollection(Arrays.asList(romeoSecret, julietSecret)); - PGPPublicKeyRingCollection verificationKeys = new PGPPublicKeyRingCollection(Arrays.asList(KeyRingUtils.publicKeyRingFrom(cryptieKeys), romeoKeys)); + PGPSecretKeyRingCollection secretKeys = new PGPSecretKeyRingCollection( + Arrays.asList(romeoSecret, julietSecret)); + PGPPublicKeyRingCollection verificationKeys = new PGPPublicKeyRingCollection( + Arrays.asList(KeyRingUtils.publicKeyRingFrom(cryptieKeys), romeoKeys)); DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() .onInputStream(cryptIn) @@ -105,31 +106,36 @@ public class SigningTest { Streams.pipeAll(decryptionStream, plaintextOut); decryptionStream.close(); - OpenPgpMetadata metadata = decryptionStream.getResult(); + MessageMetadata metadata = decryptionStream.getMetadata(); assertTrue(metadata.isEncrypted()); - assertTrue(metadata.isSigned()); - assertTrue(metadata.isVerified()); - assertTrue(metadata.containsVerifiedSignatureFrom(KeyRingUtils.publicKeyRingFrom(cryptieKeys))); - assertFalse(metadata.containsVerifiedSignatureFrom(julietKeys)); + assertTrue(metadata.isVerifiedSigned()); + assertTrue(metadata.isVerifiedSignedBy(KeyRingUtils.publicKeyRingFrom(cryptieKeys))); + assertFalse(metadata.isVerifiedSignedBy(julietKeys)); } - @Test - public void testSignWithInvalidUserIdFails() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void testSignWithInvalidUserIdFails() + throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() .modernKeyRing("alice", "password123"); - SecretKeyRingProtector protector = SecretKeyRingProtector.unlockAllKeysWith(Passphrase.fromPassword("password123"), secretKeys); + SecretKeyRingProtector protector = SecretKeyRingProtector.unlockAnyKeyWith(Passphrase.fromPassword("password123")); SigningOptions opts = new SigningOptions(); // "bob" is not a valid user-id - assertThrows(KeyValidationError.class, - () -> opts.addInlineSignature(protector, secretKeys, "bob", DocumentSignatureType.CANONICAL_TEXT_DOCUMENT)); + assertThrows(KeyException.UnboundUserIdException.class, + () -> opts.addInlineSignature(protector, secretKeys, "bob", + DocumentSignatureType.CANONICAL_TEXT_DOCUMENT)); } - @Test - public void testSignWithRevokedUserIdFails() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void testSignWithRevokedUserIdFails() + throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() .modernKeyRing("alice", "password123"); - SecretKeyRingProtector protector = SecretKeyRingProtector.unlockAllKeysWith(Passphrase.fromPassword("password123"), secretKeys); + SecretKeyRingProtector protector = SecretKeyRingProtector.unlockAnyKeyWith( + Passphrase.fromPassword("password123")); secretKeys = PGPainless.modifyKeyRing(secretKeys) .revokeUserId("alice", protector) .done(); @@ -138,11 +144,13 @@ public class SigningTest { SigningOptions opts = new SigningOptions(); // "alice" has been revoked - assertThrows(KeyValidationError.class, - () -> opts.addInlineSignature(protector, fSecretKeys, "alice", DocumentSignatureType.CANONICAL_TEXT_DOCUMENT)); + assertThrows(KeyException.UnboundUserIdException.class, + () -> opts.addInlineSignature(protector, fSecretKeys, "alice", + DocumentSignatureType.CANONICAL_TEXT_DOCUMENT)); } - @Test + @TestTemplate + @ExtendWith(TestAllImplementations.class) public void signWithHashAlgorithmOverride() throws PGPException, IOException { PGPSecretKeyRing secretKeys = TestKeys.getEmilSecretKeyRing(); SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); @@ -173,15 +181,20 @@ public class SigningTest { assertEquals(HashAlgorithm.SHA224.getAlgorithmId(), signature.getHashAlgorithm()); } - @Test - public void negotiateHashAlgorithmChoseFallbackIfEmptyPreferences() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() - .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA).overridePreferredHashAlgorithms()) + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void negotiateHashAlgorithmChoseFallbackIfEmptyPreferences() + throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() + .setPrimaryKey(KeySpec.getBuilder( + KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA) + .overridePreferredHashAlgorithms()) .addUserId("Alice") .build(); SigningOptions options = new SigningOptions() - .addDetachedSignature(SecretKeyRingProtector.unprotectedKeys(), secretKeys, DocumentSignatureType.BINARY_DOCUMENT); + .addDetachedSignature(SecretKeyRingProtector.unprotectedKeys(), secretKeys, + DocumentSignatureType.BINARY_DOCUMENT); String data = "Hello, World!\n"; EncryptionStream signer = PGPainless.encryptAndOrSign() .onOutputStream(new ByteArrayOutputStream()) @@ -194,19 +207,24 @@ public class SigningTest { SubkeyIdentifier signingKey = sigs.keySet().iterator().next(); PGPSignature signature = sigs.get(signingKey).iterator().next(); - assertEquals(PGPainless.getPolicy().getSignatureHashAlgorithmPolicy().defaultHashAlgorithm().getAlgorithmId(), signature.getHashAlgorithm()); + assertEquals(PGPainless.getPolicy().getDataSignatureHashAlgorithmPolicy().defaultHashAlgorithm().getAlgorithmId(), + signature.getHashAlgorithm()); } - @Test - public void negotiateHashAlgorithmChoseFallbackIfUnacceptablePreferences() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() - .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA) + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void negotiateHashAlgorithmChoseFallbackIfUnacceptablePreferences() + throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() + .setPrimaryKey( + KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA) .overridePreferredHashAlgorithms(HashAlgorithm.MD5)) .addUserId("Alice") .build(); SigningOptions options = new SigningOptions() - .addDetachedSignature(SecretKeyRingProtector.unprotectedKeys(), secretKeys, DocumentSignatureType.BINARY_DOCUMENT); + .addDetachedSignature(SecretKeyRingProtector.unprotectedKeys(), secretKeys, + DocumentSignatureType.BINARY_DOCUMENT); String data = "Hello, World!\n"; EncryptionStream signer = PGPainless.encryptAndOrSign() .onOutputStream(new ByteArrayOutputStream()) @@ -219,33 +237,43 @@ public class SigningTest { SubkeyIdentifier signingKey = sigs.keySet().iterator().next(); PGPSignature signature = sigs.get(signingKey).iterator().next(); - assertEquals(PGPainless.getPolicy().getSignatureHashAlgorithmPolicy().defaultHashAlgorithm().getAlgorithmId(), signature.getHashAlgorithm()); + assertEquals(PGPainless.getPolicy().getDataSignatureHashAlgorithmPolicy().defaultHashAlgorithm().getAlgorithmId(), + signature.getHashAlgorithm()); } - @Test - public void signingWithNonCapableKeyThrowsKeyCannotSignException() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() - .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER)) + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void signingWithNonCapableKeyThrowsKeyCannotSignException() + throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER)) .addUserId("Alice") .build(); SigningOptions options = new SigningOptions(); - assertThrows(KeyCannotSignException.class, () -> options.addDetachedSignature(SecretKeyRingProtector.unprotectedKeys(), secretKeys, DocumentSignatureType.BINARY_DOCUMENT)); - assertThrows(KeyCannotSignException.class, () -> options.addInlineSignature(SecretKeyRingProtector.unprotectedKeys(), secretKeys, DocumentSignatureType.BINARY_DOCUMENT)); + assertThrows(KeyException.UnacceptableSigningKeyException.class, () -> options.addDetachedSignature( + SecretKeyRingProtector.unprotectedKeys(), secretKeys, DocumentSignatureType.BINARY_DOCUMENT)); + assertThrows(KeyException.UnacceptableSigningKeyException.class, () -> options.addInlineSignature( + SecretKeyRingProtector.unprotectedKeys(), secretKeys, DocumentSignatureType.BINARY_DOCUMENT)); } - @Test - public void signWithInvalidUserIdThrowsKeyValidationError() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() - .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA)) + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void signWithInvalidUserIdThrowsKeyValidationError() + throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), + KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA)) .addUserId("Alice") .build(); SigningOptions options = new SigningOptions(); - assertThrows(KeyValidationError.class, () -> - options.addDetachedSignature(SecretKeyRingProtector.unprotectedKeys(), secretKeys, "Bob", DocumentSignatureType.BINARY_DOCUMENT)); - assertThrows(KeyValidationError.class, () -> - options.addInlineSignature(SecretKeyRingProtector.unprotectedKeys(), secretKeys, "Bob", DocumentSignatureType.BINARY_DOCUMENT)); + assertThrows(KeyException.UnboundUserIdException.class, () -> + options.addDetachedSignature(SecretKeyRingProtector.unprotectedKeys(), secretKeys, "Bob", + DocumentSignatureType.BINARY_DOCUMENT)); + assertThrows(KeyException.UnboundUserIdException.class, () -> + options.addInlineSignature(SecretKeyRingProtector.unprotectedKeys(), secretKeys, "Bob", + DocumentSignatureType.BINARY_DOCUMENT)); } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/example/ConvertKeys.java b/pgpainless-core/src/test/java/org/pgpainless/example/ConvertKeys.java index 7a9aad29..fc99fa44 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/example/ConvertKeys.java +++ b/pgpainless-core/src/test/java/org/pgpainless/example/ConvertKeys.java @@ -16,24 +16,19 @@ import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.key.info.KeyRingInfo; -import org.pgpainless.key.util.KeyRingUtils; public class ConvertKeys { /** * This example demonstrates how to extract a public key certificate from a secret key. - * - * @throws PGPException - * @throws InvalidAlgorithmParameterException - * @throws NoSuchAlgorithmException */ @Test public void secretKeyToCertificate() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { String userId = "alice@wonderland.lit"; PGPSecretKeyRing secretKey = PGPainless.generateKeyRing() - .modernKeyRing(userId, null); + .modernKeyRing(userId); // Extract certificate (public key) from secret key - PGPPublicKeyRing certificate = KeyRingUtils.publicKeyRingFrom(secretKey); + PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKey); KeyRingInfo secretKeyInfo = PGPainless.inspectKeyRing(secretKey); diff --git a/pgpainless-core/src/test/java/org/pgpainless/example/DecryptOrVerify.java b/pgpainless-core/src/test/java/org/pgpainless/example/DecryptOrVerify.java index da641325..c35b3572 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/example/DecryptOrVerify.java +++ b/pgpainless-core/src/test/java/org/pgpainless/example/DecryptOrVerify.java @@ -5,9 +5,10 @@ package org.pgpainless.example; import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; -import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -23,16 +24,20 @@ import org.pgpainless.PGPainless; import org.pgpainless.algorithm.DocumentSignatureType; import org.pgpainless.decryption_verification.ConsumerOptions; import org.pgpainless.decryption_verification.DecryptionStream; -import org.pgpainless.decryption_verification.OpenPgpMetadata; -import org.pgpainless.decryption_verification.cleartext_signatures.InMemoryMultiPassStrategy; +import org.pgpainless.decryption_verification.MessageMetadata; import org.pgpainless.encryption_signing.EncryptionStream; import org.pgpainless.encryption_signing.ProducerOptions; import org.pgpainless.encryption_signing.SigningOptions; -import org.pgpainless.exception.WrongConsumingMethodException; import org.pgpainless.key.protection.SecretKeyRingProtector; +/** + * This class contains examples on how to decrypt encrypted, and verify signed messages. + */ public class DecryptOrVerify { + /** + * The secret key. + */ private static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + "Version: PGPainless\n" + "Comment: AA21 9149 3B35 E679 8876 DE43 B0D7 8185 F639 B6C9\n" + @@ -57,6 +62,22 @@ public class DecryptOrVerify { "=JHMt\n" + "-----END PGP PRIVATE KEY BLOCK-----\n"; + /** + * Protector to unlock the secret key. + * Since the key is not protected, it is enough to use an unprotectedKeys implementation. + * + * For more info on how to use the {@link SecretKeyRingProtector}, see {@link UnlockSecretKeys}. + */ + private static final SecretKeyRingProtector keyProtector = SecretKeyRingProtector.unprotectedKeys(); + + /** + * The plaintext message. + */ + private static final String PLAINTEXT = "Hello, World!\n"; + + /** + * The {@link #PLAINTEXT} message, but signed using inband signatures. + */ private static final String INBAND_SIGNED = "-----BEGIN PGP MESSAGE-----\n" + "Version: PGPainless\n" + "\n" + @@ -66,6 +87,10 @@ public class DecryptOrVerify { "M8e7ufwA\n" + "=RDiy\n" + "-----END PGP MESSAGE-----"; + + /** + * The {@link #PLAINTEXT} message, but signed using the cleartext signature framework. + */ private static final String CLEARTEXT_SIGNED = "-----BEGIN PGP SIGNED MESSAGE-----\n" + "Hash: SHA512\n" + "\n" + @@ -80,78 +105,192 @@ public class DecryptOrVerify { "=oNIK\n" + "-----END PGP SIGNATURE-----"; + /** + * The {@link #PLAINTEXT} message, but encrypted for the {@link #certificate}. + */ + private static final String ENCRYPTED = "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "hF4DwqNy0B3ItTkSAQdArkuJHqPTVX+UaqQtHzppwOZDK0TfH1f/fAjrZaso/DUw\n" + + "ne6Xc1HYG+gTBWEQUw09m5b/f0E7DSeIg/ai/HKnF8mBSIQhphPR4yVAWypOOUmh\n" + + "0kABCiGjaJQyAzF/VtzC+ZVU67DfBl24CEPaRMumxieVUqo/VYWy3zyzE6H1zMqq\n" + + "/lWeVnK7NwtfArlhpRcph0S8\n" + + "=1cyl\n" + + "-----END PGP MESSAGE-----\n"; + + /** + * The {@link #PLAINTEXT} message signed by the {@link #secretKey} and encrypted for the {@link #certificate}. + */ + private static final String ENCRYPTED_AND_SIGNED = "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "hF4DwqNy0B3ItTkSAQdAGqwFJ6SRW6It9w+RBudeGbdUj8OZqwApqyvwbKUzJiYw\n" + + "WAcJOrGIbrK9bKzJdCLbVYkegILb6vqTuamU8iYDCccstV4Y2w0kT5ynHHPVFKfg\n" + + "0r8BUe/Mi8zL0Af6K2r6A9gq/Q8vmscoOB5mI5Yxrk48+rPcp0rZbSu9rC9pHZfs\n" + + "hhvxwGwG8EZm14pseHUZdoKldUD8tCbhkS7wDMOHzA1Fo1m1Yyjhe4kBaCrn9zhP\n" + + "YSeOzHtMxk5JBcrZW+LMMuRGNBzxc0R1yirqk8yymF1qzTTuYqziO0QxbW1gU00F\n" + + "ewdovd7Cx1Il8ONgRzGS3Wyb+iORNuhLpw+w2SV74Kg8XWLD7pDFgOuFZw39b+0X\n" + + "Nw==\n" + + "=9PiO\n" + + "-----END PGP MESSAGE-----"; + private static PGPSecretKeyRing secretKey; private static PGPPublicKeyRing certificate; @BeforeAll public static void prepare() throws IOException { + // read the secret key secretKey = PGPainless.readKeyRing().secretKeyRing(KEY); + // certificate is the public part of the key certificate = PGPainless.extractCertificate(secretKey); } + /** + * This example demonstrates how to decrypt an encrypted message using a secret key. + * + * @throws PGPException + * @throws IOException + */ + @Test + public void decryptMessage() throws PGPException, IOException { + ConsumerOptions consumerOptions = new ConsumerOptions() + .addDecryptionKey(secretKey, keyProtector); // add the decryption key ring + + ByteArrayOutputStream plaintextOut = new ByteArrayOutputStream(); + ByteArrayInputStream ciphertextIn = new ByteArrayInputStream(ENCRYPTED.getBytes(StandardCharsets.UTF_8)); + + // The decryption stream is an input stream from which we read the decrypted data + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(ciphertextIn) + .withOptions(consumerOptions); + + Streams.pipeAll(decryptionStream, plaintextOut); + decryptionStream.close(); // remember to close the stream! + + // The metadata object contains information about the message + MessageMetadata metadata = decryptionStream.getMetadata(); + assertTrue(metadata.isEncrypted()); // message was encrypted + assertTrue(metadata.isEncryptedFor(secretKey)); + assertFalse(metadata.isVerifiedSigned()); // We did not do any signature verification + + // The output stream now contains the decrypted message + assertEquals(PLAINTEXT, plaintextOut.toString()); + } + + /** + * In this example, an encrypted and signed message is processed. + * The message gets decrypted using the secret key and the signatures are verified using the certificate. + * + * @throws PGPException + * @throws IOException + */ + @Test + public void decryptMessageAndVerifySignatures() throws PGPException, IOException { + ConsumerOptions consumerOptions = new ConsumerOptions() + .addDecryptionKey(secretKey, keyProtector) // provide the secret key of the recipient for decryption + .addVerificationCert(certificate); // provide the signers public key for signature verification + + ByteArrayOutputStream plaintextOut = new ByteArrayOutputStream(); + ByteArrayInputStream ciphertextIn = new ByteArrayInputStream(ENCRYPTED_AND_SIGNED.getBytes(StandardCharsets.UTF_8)); + + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(ciphertextIn) + .withOptions(consumerOptions); + + Streams.pipeAll(decryptionStream, plaintextOut); + decryptionStream.close(); // remember to close the stream to finish signature verification + + // metadata with information on the message, like signatures + MessageMetadata metadata = decryptionStream.getMetadata(); + assertTrue(metadata.isEncrypted()); // messages was in fact encrypted + assertTrue(metadata.isEncryptedFor(certificate)); + assertTrue(metadata.isVerifiedSigned()); // the signatures were actually correct + assertTrue(metadata.isVerifiedSignedBy(certificate)); // the signatures could be verified using the certificate + + assertEquals(PLAINTEXT, plaintextOut.toString()); + } + + /** + * In this example, signed messages are verified. + * The example shows that verification of inband signed, and cleartext signed messages works the same. + * @throws PGPException + * @throws IOException + */ @Test public void verifySignatures() throws PGPException, IOException { ConsumerOptions options = new ConsumerOptions() - .addVerificationCert(certificate); + .addVerificationCert(certificate); // provide the signers certificate for verification of signatures for (String signed : new String[] {INBAND_SIGNED, CLEARTEXT_SIGNED}) { ByteArrayOutputStream out = new ByteArrayOutputStream(); ByteArrayInputStream in = new ByteArrayInputStream(signed.getBytes(StandardCharsets.UTF_8)); - BufferedInputStream bufIn = new BufferedInputStream(in); - bufIn.mark(512); - DecryptionStream verificationStream; - try { - verificationStream = PGPainless.decryptAndOrVerify() - .onInputStream(bufIn) - .withOptions(options); - } catch (WrongConsumingMethodException e) { - bufIn.reset(); - // Cleartext Signed Message - verificationStream = PGPainless.verifyCleartextSignedMessage() - .onInputStream(bufIn) - .withStrategy(new InMemoryMultiPassStrategy()) - .withOptions(options) - .getVerificationStream(); - } + + DecryptionStream verificationStream = PGPainless.decryptAndOrVerify() + .onInputStream(in) + .withOptions(options); Streams.pipeAll(verificationStream, out); - verificationStream.close(); + verificationStream.close(); // remember to close the stream to finish sig verification - OpenPgpMetadata metadata = verificationStream.getResult(); - assertTrue(metadata.isVerified()); - assertArrayEquals("Hello, World!\n".getBytes(StandardCharsets.UTF_8), out.toByteArray()); + // Get the metadata object for information about the message + MessageMetadata metadata = verificationStream.getMetadata(); + assertTrue(metadata.isVerifiedSigned()); // signatures were verified successfully + assertTrue(metadata.isVerifiedSignedBy(certificate)); + // The output stream we piped to now contains the message + assertEquals(PLAINTEXT, out.toString()); } } - + /** + * This example shows how to create - and verify - cleartext signed messages. + * @throws PGPException + * @throws IOException + */ @Test - public void createVerifyCleartextSignedMessage() throws PGPException, IOException { + public void createAndVerifyCleartextSignedMessage() throws PGPException, IOException { + // In this example we sign and verify a number of different messages one after the other for (String msg : new String[] {"Hello World!", "- Hello - World -", "Hello, World!\n", "Hello\nWorld!"}) { + // we need to read the plaintext message from somewhere ByteArrayInputStream in = new ByteArrayInputStream(msg.getBytes(StandardCharsets.UTF_8)); + // and write the signed message to an output stream ByteArrayOutputStream out = new ByteArrayOutputStream(); + + SigningOptions signingOptions = SigningOptions.get(); + // for cleartext signed messages, we need to add a detached signature... + signingOptions.addDetachedSignature(keyProtector, secretKey, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT); + ProducerOptions producerOptions = ProducerOptions.sign(signingOptions) + .setCleartextSigned(); // and declare that the message will be cleartext signed + + // Create the signing stream EncryptionStream signingStream = PGPainless.encryptAndOrSign() - .onOutputStream(out) - .withOptions(ProducerOptions.sign(SigningOptions.get() - .addDetachedSignature(SecretKeyRingProtector.unprotectedKeys(), secretKey, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT) - ).setCleartextSigned()); + .onOutputStream(out) // on the output stream + .withOptions(producerOptions); // with the options - Streams.pipeAll(in, signingStream); - signingStream.close(); + Streams.pipeAll(in, signingStream); // pipe the plaintext message into the signing stream + signingStream.close(); // remember to close the stream to finish the signatures - ByteArrayInputStream signedIn = new ByteArrayInputStream(out.toByteArray()); + // Now the output stream contains the signed message + byte[] signedMessage = out.toByteArray(); - DecryptionStream verificationStream = PGPainless.verifyCleartextSignedMessage() + // Verification + // we need to read the signed message + ByteArrayInputStream signedIn = new ByteArrayInputStream(signedMessage); + + // and pass it to the decryption stream + DecryptionStream verificationStream = PGPainless.decryptAndOrVerify() .onInputStream(signedIn) - .withStrategy(new InMemoryMultiPassStrategy()) - .withOptions(new ConsumerOptions().addVerificationCert(certificate)) - .getVerificationStream(); + .withOptions(new ConsumerOptions().addVerificationCert(certificate)); + // plain will receive the plaintext message ByteArrayOutputStream plain = new ByteArrayOutputStream(); Streams.pipeAll(verificationStream, plain); - verificationStream.close(); - OpenPgpMetadata metadata = verificationStream.getResult(); - assertTrue(metadata.isVerified()); + verificationStream.close(); // as always, remember to close the stream + + // Metadata will confirm that the message was in fact signed + MessageMetadata metadata = verificationStream.getMetadata(); + assertTrue(metadata.isVerifiedSigned()); + // compare the plaintext to what we originally signed assertArrayEquals(msg.getBytes(StandardCharsets.UTF_8), plain.toByteArray()); } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/example/Encrypt.java b/pgpainless-core/src/test/java/org/pgpainless/example/Encrypt.java index 85266a31..d97891d8 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/example/Encrypt.java +++ b/pgpainless-core/src/test/java/org/pgpainless/example/Encrypt.java @@ -11,8 +11,6 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKeyRing; @@ -23,17 +21,108 @@ import org.pgpainless.PGPainless; import org.pgpainless.algorithm.DocumentSignatureType; import org.pgpainless.decryption_verification.ConsumerOptions; import org.pgpainless.decryption_verification.DecryptionStream; -import org.pgpainless.decryption_verification.OpenPgpMetadata; +import org.pgpainless.decryption_verification.MessageMetadata; import org.pgpainless.encryption_signing.EncryptionOptions; import org.pgpainless.encryption_signing.EncryptionStream; import org.pgpainless.encryption_signing.ProducerOptions; import org.pgpainless.encryption_signing.SigningOptions; import org.pgpainless.key.protection.SecretKeyRingProtector; -import org.pgpainless.key.util.KeyRingUtils; import org.pgpainless.util.Passphrase; public class Encrypt { + private static final String ALICE_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 12E3 4F04 C66D 2B70 D16C 960D ACF2 16F0 F93D DD20\n" + + "Comment: alice@pgpainless.org\n" + + "\n" + + "lFgEYksu1hYJKwYBBAHaRw8BAQdAIhUpRrs6zFTBI1pK40jCkzY/DQ/t4fUgNtlS\n" + + "mXOt1cIAAP4wM0LQD/Wj9w6/QujM/erj/TodDZzmp2ZwblrvDQri0RJ/tBRhbGlj\n" + + "ZUBwZ3BhaW5sZXNzLm9yZ4iPBBMWCgBBBQJiSy7WCRCs8hbw+T3dIBYhBBLjTwTG\n" + + "bStw0WyWDazyFvD5Pd0gAp4BApsBBRYCAwEABAsJCAcFFQoJCAsCmQEAAOOTAQDf\n" + + "UsRQSAs0d/Nm4YIrq+gU7gOdTJuf33f/u/u1nGM1fAD/RY7I3gQoZ0lWbvXVkRAL\n" + + "Cu9cUJdvL7kpW1oYtYg21QucXQRiSy7WEgorBgEEAZdVAQUBAQdA60F84k6MY/Uy\n" + + "BCZe4/WP8JDw/Efu5/Gyk8hcd3HzHFsDAQgHAAD/aC8DOOkK0XNVz2hkSVczmNoJ\n" + + "Umog0PfQLRujpOTqonAQKIh1BBgWCgAdBQJiSy7WAp4BApsMBRYCAwEABAsJCAcF\n" + + "FQoJCAsACgkQrPIW8Pk93SCd6AD/Y3LF2RvgbEaOBtAvH6w0ZBPorB3rk6dx+Ae0\n" + + "GvW4E8wA+QHmgNo0pdkDxTl0BN1KC7BV1iRFqe9Vo7fW2LLfhlEEnFgEYksu1hYJ\n" + + "KwYBBAHaRw8BAQdAPtqap21/zmVzxOHk++891/EZSNikwWkq9t0pmYjhtJ8AAP9N\n" + + "m/G6nbiEB8mu/TkNnb7vdhSmLddL9kdKh0LzWD95LBF0iNUEGBYKAH0FAmJLLtYC\n" + + "ngECmwIFFgIDAQAECwkIBwUVCgkIC18gBBkWCgAGBQJiSy7WAAoJEOEz2Vo79Yyl\n" + + "zN0A/iZAVklSJsfQslshR6/zMBufwCK1S05jg/5Ydaksv3QcAQC4gsxdFFne+H4M\n" + + "mos4atad6hMhlqr0/Zyc71ZdO5I/CAAKCRCs8hbw+T3dIGhqAQCIdVtCus336cDe\n" + + "Nug+E9v1PEM3F/dt6GAqSG8LJqdAGgEA8cUXdUBooOo/QBkDnpteke8Z3IhIGyGe\n" + + "dc8OwJyVFwc=\n" + + "=ARAi\n" + + "-----END PGP PRIVATE KEY BLOCK-----\n"; + private static final String ALICE_CERT = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 12E3 4F04 C66D 2B70 D16C 960D ACF2 16F0 F93D DD20\n" + + "Comment: alice@pgpainless.org\n" + + "\n" + + "mDMEYksu1hYJKwYBBAHaRw8BAQdAIhUpRrs6zFTBI1pK40jCkzY/DQ/t4fUgNtlS\n" + + "mXOt1cK0FGFsaWNlQHBncGFpbmxlc3Mub3JniI8EExYKAEEFAmJLLtYJEKzyFvD5\n" + + "Pd0gFiEEEuNPBMZtK3DRbJYNrPIW8Pk93SACngECmwEFFgIDAQAECwkIBwUVCgkI\n" + + "CwKZAQAA45MBAN9SxFBICzR382bhgiur6BTuA51Mm5/fd/+7+7WcYzV8AP9Fjsje\n" + + "BChnSVZu9dWREAsK71xQl28vuSlbWhi1iDbVC7g4BGJLLtYSCisGAQQBl1UBBQEB\n" + + "B0DrQXziToxj9TIEJl7j9Y/wkPD8R+7n8bKTyFx3cfMcWwMBCAeIdQQYFgoAHQUC\n" + + "Yksu1gKeAQKbDAUWAgMBAAQLCQgHBRUKCQgLAAoJEKzyFvD5Pd0gnegA/2Nyxdkb\n" + + "4GxGjgbQLx+sNGQT6Kwd65OncfgHtBr1uBPMAPkB5oDaNKXZA8U5dATdSguwVdYk\n" + + "RanvVaO31tiy34ZRBLgzBGJLLtYWCSsGAQQB2kcPAQEHQD7amqdtf85lc8Th5Pvv\n" + + "PdfxGUjYpMFpKvbdKZmI4bSfiNUEGBYKAH0FAmJLLtYCngECmwIFFgIDAQAECwkI\n" + + "BwUVCgkIC18gBBkWCgAGBQJiSy7WAAoJEOEz2Vo79YylzN0A/iZAVklSJsfQslsh\n" + + "R6/zMBufwCK1S05jg/5Ydaksv3QcAQC4gsxdFFne+H4Mmos4atad6hMhlqr0/Zyc\n" + + "71ZdO5I/CAAKCRCs8hbw+T3dIGhqAQCIdVtCus336cDeNug+E9v1PEM3F/dt6GAq\n" + + "SG8LJqdAGgEA8cUXdUBooOo/QBkDnpteke8Z3IhIGyGedc8OwJyVFwc=\n" + + "=GUhm\n" + + "-----END PGP PUBLIC KEY BLOCK-----\n"; + + private static final String BOB_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: A0D2 F316 0F6B 2CE5 7A50 FF32 261E 5081 9736 C493\n" + + "Comment: bob@pgpainless.org\n" + + "\n" + + "lFgEYksu1hYJKwYBBAHaRw8BAQdAXTBT1OKN1GAvGC+fzuy/k34BK+d5Saa87Glb\n" + + "iQgIxg8AAPwMI5DGqADFfl6H3Nxj3NxEZLasiFDpwEszluLVRy0jihGbtBJib2JA\n" + + "cGdwYWlubGVzcy5vcmeIjwQTFgoAQQUCYksu1gkQJh5QgZc2xJMWIQSg0vMWD2ss\n" + + "5XpQ/zImHlCBlzbEkwKeAQKbAQUWAgMBAAQLCQgHBRUKCQgLApkBAADvrAD/cWBW\n" + + "mRkSfoCbEl22s59FXE7NPENrsJK8jxmWsWX3jbEA/AyXMCjwH6IhDgdgO7wH2z1r\n" + + "cUb/hokiCcCaJs6hjKcInF0EYksu1hIKKwYBBAGXVQEFAQEHQCeURSBi9brhisUH\n" + + "Dz0xN1NCgU5yeirx53xrQDFFx+d6AwEIBwAA/1GHX9+4Rg0ePsXGm1QIWL+C4rdf\n" + + "AReCTYoS3EBiZVdADoyIdQQYFgoAHQUCYksu1gKeAQKbDAUWAgMBAAQLCQgHBRUK\n" + + "CQgLAAoJECYeUIGXNsST8c0A/1dEIO9gsFB15UWDlTzN3S0TXQNN8wVzIMdW7XP2\n" + + "7c6bAQCB5ChqQA9AB1020DLr28BAbSjI7mPdIWg2PpE7B1EXC5xYBGJLLtYWCSsG\n" + + "AQQB2kcPAQEHQKP5NxT0ZhmRbrl3S6uwrUN248g1TEUR0DCVuLgyGSLpAAEA6bMa\n" + + "GaUf3S55rkFDjFC4Cv72zc8E5ex2RKgbpxXxqhYQN4jVBBgWCgB9BQJiSy7WAp4B\n" + + "ApsCBRYCAwEABAsJCAcFFQoJCAtfIAQZFgoABgUCYksu1gAKCRDJLjPCA2NIfylD\n" + + "AP4tNFV23FBlrC57iesHVc+TTfNJ8rd+U7mbJvUgykcSNAEAy64tKPuVj+aA1bpm\n" + + "gHxfqdEJCOko8UhVVP6ltiDUcAoACgkQJh5QgZc2xJP9TQEA1DNgFno3di+xGDEN\n" + + "pwe9lmz8d/RWy/kuBT9S/3CMJjQBAKNBhHPuFfvk7RFbsmMrHsSqDFqIuUfGqq39\n" + + "VzmiMp8N\n" + + "=LpkJ\n" + + "-----END PGP PRIVATE KEY BLOCK-----\n"; + private static final String BOB_CERT = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: A0D2 F316 0F6B 2CE5 7A50 FF32 261E 5081 9736 C493\n" + + "Comment: bob@pgpainless.org\n" + + "\n" + + "mDMEYksu1hYJKwYBBAHaRw8BAQdAXTBT1OKN1GAvGC+fzuy/k34BK+d5Saa87Glb\n" + + "iQgIxg+0EmJvYkBwZ3BhaW5sZXNzLm9yZ4iPBBMWCgBBBQJiSy7WCRAmHlCBlzbE\n" + + "kxYhBKDS8xYPayzlelD/MiYeUIGXNsSTAp4BApsBBRYCAwEABAsJCAcFFQoJCAsC\n" + + "mQEAAO+sAP9xYFaZGRJ+gJsSXbazn0VcTs08Q2uwkryPGZaxZfeNsQD8DJcwKPAf\n" + + "oiEOB2A7vAfbPWtxRv+GiSIJwJomzqGMpwi4OARiSy7WEgorBgEEAZdVAQUBAQdA\n" + + "J5RFIGL1uuGKxQcPPTE3U0KBTnJ6KvHnfGtAMUXH53oDAQgHiHUEGBYKAB0FAmJL\n" + + "LtYCngECmwwFFgIDAQAECwkIBwUVCgkICwAKCRAmHlCBlzbEk/HNAP9XRCDvYLBQ\n" + + "deVFg5U8zd0tE10DTfMFcyDHVu1z9u3OmwEAgeQoakAPQAddNtAy69vAQG0oyO5j\n" + + "3SFoNj6ROwdRFwu4MwRiSy7WFgkrBgEEAdpHDwEBB0Cj+TcU9GYZkW65d0ursK1D\n" + + "duPINUxFEdAwlbi4Mhki6YjVBBgWCgB9BQJiSy7WAp4BApsCBRYCAwEABAsJCAcF\n" + + "FQoJCAtfIAQZFgoABgUCYksu1gAKCRDJLjPCA2NIfylDAP4tNFV23FBlrC57iesH\n" + + "Vc+TTfNJ8rd+U7mbJvUgykcSNAEAy64tKPuVj+aA1bpmgHxfqdEJCOko8UhVVP6l\n" + + "tiDUcAoACgkQJh5QgZc2xJP9TQEA1DNgFno3di+xGDENpwe9lmz8d/RWy/kuBT9S\n" + + "/3CMJjQBAKNBhHPuFfvk7RFbsmMrHsSqDFqIuUfGqq39VzmiMp8N\n" + + "=1MqZ\n" + + "-----END PGP PUBLIC KEY BLOCK-----\n"; + /** * In this example, Alice is sending a signed and encrypted message to Bob. * She signs the message using her key and then encrypts the message to both bobs certificate and her own. @@ -42,16 +131,14 @@ public class Encrypt { * her certificate. */ @Test - public void encryptAndSignMessage() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + public void encryptAndSignMessage() throws PGPException, IOException { // Prepare keys - PGPSecretKeyRing keyAlice = PGPainless.generateKeyRing() - .modernKeyRing("alice@pgpainless.org", null); - PGPPublicKeyRing certificateAlice = KeyRingUtils.publicKeyRingFrom(keyAlice); + PGPSecretKeyRing keyAlice = PGPainless.readKeyRing().secretKeyRing(ALICE_KEY); + PGPPublicKeyRing certificateAlice = PGPainless.readKeyRing().publicKeyRing(ALICE_CERT); SecretKeyRingProtector protectorAlice = SecretKeyRingProtector.unprotectedKeys(); - PGPSecretKeyRing keyBob = PGPainless.generateKeyRing() - .modernKeyRing("bob@pgpainless.org", null); - PGPPublicKeyRing certificateBob = KeyRingUtils.publicKeyRingFrom(keyBob); + PGPSecretKeyRing keyBob = PGPainless.readKeyRing().secretKeyRing(BOB_KEY); + PGPPublicKeyRing certificateBob = PGPainless.readKeyRing().publicKeyRing(BOB_CERT); SecretKeyRingProtector protectorBob = SecretKeyRingProtector.unprotectedKeys(); // plaintext message to encrypt @@ -61,12 +148,12 @@ public class Encrypt { EncryptionStream encryptor = PGPainless.encryptAndOrSign() .onOutputStream(ciphertext) .withOptions(ProducerOptions.signAndEncrypt( - // we want to encrypt communication (affects key selection based on key flags) - EncryptionOptions.encryptCommunications() - .addRecipient(certificateBob) - .addRecipient(certificateAlice), - new SigningOptions() - .addInlineSignature(protectorAlice, keyAlice, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT) + // we want to encrypt communication (affects key selection based on key flags) + EncryptionOptions.encryptCommunications() + .addRecipient(certificateBob) + .addRecipient(certificateAlice), + new SigningOptions() + .addInlineSignature(protectorAlice, keyAlice, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT) ).setAsciiArmor(true) ); @@ -89,18 +176,15 @@ public class Encrypt { decryptor.close(); // Check the metadata to see how the message was encrypted/signed - OpenPgpMetadata metadata = decryptor.getResult(); + MessageMetadata metadata = decryptor.getMetadata(); assertTrue(metadata.isEncrypted()); - assertTrue(metadata.containsVerifiedSignatureFrom(certificateAlice)); + assertTrue(metadata.isVerifiedSignedBy(certificateAlice)); assertEquals(message, plaintext.toString()); } /** * This example demonstrates how to encrypt and decrypt a message using a passphrase. * This method can be combined with public key based encryption and signing. - * - * @throws PGPException - * @throws IOException */ @Test public void encryptUsingPassphrase() throws PGPException, IOException { @@ -111,7 +195,7 @@ public class Encrypt { .onOutputStream(ciphertext) .withOptions(ProducerOptions .encrypt(EncryptionOptions.encryptCommunications() - .addPassphrase(Passphrase.fromPassword("p4ssphr4s3")) + .addMessagePassphrase(Passphrase.fromPassword("p4ssphr4s3")) ).setAsciiArmor(true) ); @@ -123,7 +207,7 @@ public class Encrypt { // Decrypt DecryptionStream decryptor = PGPainless.decryptAndOrVerify() .onInputStream(new ByteArrayInputStream(asciiCiphertext.getBytes(StandardCharsets.UTF_8))) - .withOptions(new ConsumerOptions().addDecryptionPassphrase(Passphrase.fromPassword("p4ssphr4s3"))); + .withOptions(new ConsumerOptions().addMessagePassphrase(Passphrase.fromPassword("p4ssphr4s3"))); ByteArrayOutputStream plaintext = new ByteArrayOutputStream(); Streams.pipeAll(decryptor, plaintext); @@ -132,4 +216,73 @@ public class Encrypt { assertEquals(message, plaintext.toString()); } + + /** + * In this example, Alice is sending a signed and encrypted message to Bob. + * She encrypts the message to both bobs certificate and her own. + * A multiline comment header is added using the fluent ProducerOption syntax. + * + * Bob subsequently decrypts the message using his key. + */ + @Test + public void encryptWithCommentHeader() throws PGPException, IOException { + // Prepare keys + PGPPublicKeyRing certificateAlice = PGPainless.readKeyRing().publicKeyRing(ALICE_CERT); + + PGPSecretKeyRing keyBob = PGPainless.readKeyRing().secretKeyRing(BOB_KEY); + PGPPublicKeyRing certificateBob = PGPainless.readKeyRing().publicKeyRing(BOB_CERT); + SecretKeyRingProtector protectorBob = SecretKeyRingProtector.unprotectedKeys(); + + // plaintext message to encrypt + String message = "Hello, World!\n"; + String[] comments = { + "This comment was added using options.", + "And it has three lines.", + " ", + "Empty lines are skipped." + }; + String comment = comments[0] + "\n" + comments[1] + "\n" + comments[2] + "\n" + comments[3]; + ByteArrayOutputStream ciphertext = new ByteArrayOutputStream(); + // Encrypt and sign + EncryptionStream encryptor = PGPainless.encryptAndOrSign() + .onOutputStream(ciphertext) + .withOptions(ProducerOptions.encrypt( + // we want to encrypt communication (affects key selection based on key flags) + EncryptionOptions.encryptCommunications() + .addRecipient(certificateBob) + .addRecipient(certificateAlice) + ).setAsciiArmor(true) + .setComment(comment) + ); + + // Pipe data trough and CLOSE the stream (important) + Streams.pipeAll(new ByteArrayInputStream(message.getBytes(StandardCharsets.UTF_8)), encryptor); + encryptor.close(); + String encryptedMessage = ciphertext.toString(); + + // check that comment header was added after "BEGIN PGP" and "Version:" + assertEquals(encryptedMessage.split("\n")[2].trim(), "Comment: " + comments[0]); + assertEquals(encryptedMessage.split("\n")[3].trim(), "Comment: " + comments[1]); + assertEquals(encryptedMessage.split("\n")[4].trim(), "Comment: " + comments[3]); + + // also test, that decryption still works... + + // Decrypt and verify signatures + DecryptionStream decryptor = PGPainless.decryptAndOrVerify() + .onInputStream(new ByteArrayInputStream(encryptedMessage.getBytes(StandardCharsets.UTF_8))) + .withOptions(new ConsumerOptions() + .addDecryptionKey(keyBob, protectorBob) + .addVerificationCert(certificateAlice) + ); + + ByteArrayOutputStream plaintext = new ByteArrayOutputStream(); + + Streams.pipeAll(decryptor, plaintext); + decryptor.close(); + + // Check the metadata to see how the message was encrypted/signed + MessageMetadata metadata = decryptor.getMetadata(); + assertTrue(metadata.isEncrypted()); + assertEquals(message, plaintext.toString()); + } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/example/GenerateKeys.java b/pgpainless-core/src/test/java/org/pgpainless/example/GenerateKeys.java index 6a791e19..d6bcb0b1 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/example/GenerateKeys.java +++ b/pgpainless-core/src/test/java/org/pgpainless/example/GenerateKeys.java @@ -27,10 +27,9 @@ import org.pgpainless.key.generation.KeySpec; import org.pgpainless.key.generation.KeySpecBuilder; import org.pgpainless.key.generation.type.KeyType; import org.pgpainless.key.generation.type.ecc.EllipticCurve; -import org.pgpainless.key.generation.type.eddsa.EdDSACurve; +import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve; import org.pgpainless.key.generation.type.rsa.RsaLength; import org.pgpainless.key.info.KeyRingInfo; -import org.pgpainless.key.util.KeyRingUtils; import org.pgpainless.key.util.UserId; import org.pgpainless.util.Passphrase; @@ -38,8 +37,8 @@ import org.pgpainless.util.Passphrase; * This class demonstrates how to use PGPainless to generate secret keys. * In general the starting point for generating secret keys using PGPainless is {@link PGPainless#generateKeyRing()}. * The result ({@link org.pgpainless.key.generation.KeyRingBuilder}) provides some factory methods for key archetypes - * such as {@link org.pgpainless.key.generation.KeyRingBuilder#modernKeyRing(String, String)} or - * {@link org.pgpainless.key.generation.KeyRingBuilder#simpleRsaKeyRing(String, RsaLength)}. + * such as {@link org.pgpainless.key.generation.KeyRingTemplates#modernKeyRing(CharSequence, String)} or + * {@link org.pgpainless.key.generation.KeyRingTemplates#simpleRsaKeyRing(CharSequence, RsaLength)}. * * Those methods always take a user-id which is used as primary user-id, as well as a passphrase which is used to encrypt * the secret key. @@ -55,13 +54,10 @@ public class GenerateKeys { * encryption subkey. * * This is the recommended way to generate OpenPGP keys with PGPainless. - * - * @throws PGPException - * @throws InvalidAlgorithmParameterException - * @throws NoSuchAlgorithmException */ @Test - public void generateModernEcKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + public void generateModernEcKey() + throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { // Define a primary user-id String userId = "gbaker@pgpainless.org"; // Set a password to protect the secret key @@ -70,34 +66,31 @@ public class GenerateKeys { PGPSecretKeyRing secretKey = PGPainless.generateKeyRing() .modernKeyRing(userId, password); // Extract public key - PGPPublicKeyRing publicKey = KeyRingUtils.publicKeyRingFrom(secretKey); + PGPPublicKeyRing publicKey = PGPainless.extractCertificate(secretKey); // Encode the public key to an ASCII armored string ready for sharing String asciiArmoredPublicKey = PGPainless.asciiArmor(publicKey); - + assertTrue(asciiArmoredPublicKey.startsWith("-----BEGIN PGP PUBLIC KEY BLOCK-----")); KeyRingInfo keyInfo = new KeyRingInfo(secretKey); assertEquals(3, keyInfo.getSecretKeys().size()); assertEquals(userId, keyInfo.getPrimaryUserId()); - assertEquals(PublicKeyAlgorithm.EDDSA.getAlgorithmId(), + assertEquals(PublicKeyAlgorithm.EDDSA_LEGACY.getAlgorithmId(), keyInfo.getPublicKey().getAlgorithm()); - assertEquals(PublicKeyAlgorithm.EDDSA.getAlgorithmId(), + assertEquals(PublicKeyAlgorithm.EDDSA_LEGACY.getAlgorithmId(), keyInfo.getSigningSubkeys().get(0).getAlgorithm()); assertEquals(PublicKeyAlgorithm.ECDH.getAlgorithmId(), - keyInfo.getEncryptionSubkeys(EncryptionPurpose.STORAGE_AND_COMMUNICATIONS).get(0).getAlgorithm()); + keyInfo.getEncryptionSubkeys(EncryptionPurpose.ANY).get(0).getAlgorithm()); } /** - * This example demonstrates how to generate a simple OpenPGP key consisting of a 4096 bit RSA key. + * This example demonstrates how to generate a simple OpenPGP key consisting of a 4096-bit RSA key. * The RSA key is used for both signing and certifying, as well as encryption. * * This method is recommended if the application has to deal with legacy clients with poor algorithm support. - * - * @throws PGPException - * @throws InvalidAlgorithmParameterException - * @throws NoSuchAlgorithmException */ @Test - public void generateSimpleRSAKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + public void generateSimpleRSAKey() + throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { // Define a primary user-id String userId = "mpage@pgpainless.org"; // Set a password to protect the secret key @@ -106,7 +99,6 @@ public class GenerateKeys { PGPSecretKeyRing secretKey = PGPainless.generateKeyRing() .simpleRsaKeyRing(userId, RsaLength._4096, password); - KeyRingInfo keyInfo = new KeyRingInfo(secretKey); assertEquals(1, keyInfo.getSecretKeys().size()); assertEquals(userId, keyInfo.getPrimaryUserId()); @@ -115,17 +107,14 @@ public class GenerateKeys { /** * This example demonstrates how to generate a simple OpenPGP key based on elliptic curves. - * The key consists of an ECDSA primary key that is used both for certification of subkeys, as well as signing of data, + * The key consists of an ECDSA primary key that is used both for certification of subkeys, and signing of data, * and a single ECDH encryption subkey. * * This method is recommended if small keys and high performance are desired. - * - * @throws PGPException - * @throws InvalidAlgorithmParameterException - * @throws NoSuchAlgorithmException */ @Test - public void generateSimpleECKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + public void generateSimpleECKey() + throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { // Define a primary user-id String userId = "mhelms@pgpainless.org"; // Set a password to protect the secret key @@ -152,7 +141,7 @@ public class GenerateKeys { * {@link KeySpec} objects can best be obtained by using the {@link KeySpec#getBuilder(KeyType, KeyFlag, KeyFlag...)} * method and providing a {@link KeyType}. * There are a bunch of factory methods for different {@link KeyType} implementations present in {@link KeyType} itself - * (such as {@link KeyType#ECDH(EllipticCurve)}. {@link KeyFlag KeyFlags} determine + * (such as {@link KeyType#ECDH(EllipticCurve)}). {@link KeyFlag KeyFlags} determine * the use of the key, like encryption, signing data or certifying subkeys. * * If you so desire, you can now specify your own algorithm preferences. @@ -166,7 +155,7 @@ public class GenerateKeys { * make sure that the primary key spec has the {@link KeyFlag} {@link KeyFlag#CERTIFY_OTHER} set, as this is a requirement * for primary keys. * - * Furthermore you have to set at least the primary user-id via + * Furthermore, you have to set at least the primary user-id via * {@link org.pgpainless.key.generation.KeyRingBuilder#addUserId(String)}, * but you can also add additional user-ids. * @@ -174,13 +163,10 @@ public class GenerateKeys { * {@link org.pgpainless.key.generation.KeyRingBuilder#setExpirationDate(Date)}. * Lastly you can decide whether to set a passphrase to protect the secret key using * {@link org.pgpainless.key.generation.KeyRingBuilder#setPassphrase(Passphrase)}. - * - * @throws PGPException - * @throws InvalidAlgorithmParameterException - * @throws NoSuchAlgorithmException */ @Test - public void generateCustomOpenPGPKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + public void generateCustomOpenPGPKey() + throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { // Instead of providing a string, we can assemble a user-id by using the user-id builder. // The example below corresponds to "Morgan Carpenter (Pride!) " UserId userId = UserId.newBuilder() @@ -193,19 +179,19 @@ public class GenerateKeys { // It is recommended to use the Passphrase class, as it can be used to safely invalidate passwords from memory Passphrase passphrase = Passphrase.fromPassword("1nters3x"); - PGPSecretKeyRing secretKey = PGPainless.generateKeyRing() - .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), + PGPSecretKeyRing secretKey = PGPainless.buildKeyRing() + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), // The primary key MUST carry the CERTIFY_OTHER flag, but CAN carry additional flags KeyFlag.CERTIFY_OTHER)) // Add the first subkey (in this case encryption) .addSubkey(KeySpec.getBuilder( // We choose an ECDH key over the brainpoolp256r1 curve KeyType.ECDH(EllipticCurve._BRAINPOOLP256R1), - // Our key can encrypt both communication data, as well as data at rest + // Our key can encrypt both communication data, and data at rest KeyFlag.ENCRYPT_STORAGE, KeyFlag.ENCRYPT_COMMS ) // Optionally: Configure the subkey with custom algorithm preferences - // Is is recommended though to go with PGPainless' defaults which can be found in the + // It is recommended though to go with PGPainless' defaults which can be found in the // AlgorithmSuite class. .overridePreferredSymmetricKeyAlgorithms(SymmetricKeyAlgorithm.AES_256, SymmetricKeyAlgorithm.AES_192, SymmetricKeyAlgorithm.AES_128) .overridePreferredHashAlgorithms(HashAlgorithm.SHA512, HashAlgorithm.SHA384, HashAlgorithm.SHA256) diff --git a/pgpainless-core/src/test/java/org/pgpainless/example/ManagePolicy.java b/pgpainless-core/src/test/java/org/pgpainless/example/ManagePolicy.java index c762feca..3b29e35d 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/example/ManagePolicy.java +++ b/pgpainless-core/src/test/java/org/pgpainless/example/ManagePolicy.java @@ -26,7 +26,7 @@ import org.pgpainless.util.NotationRegistry; * Note, that PGPainless distinguishes between hash algorithms used in revocation and non-revocation signatures, * and has different policies for those. * - * Furthermore PGPainless has policies for symmetric encryption algorithms (both for encrypting and decrypting), + * Furthermore, PGPainless has policies for symmetric encryption algorithms (both for encrypting and decrypting), * for public key algorithms and key lengths, as well as compression algorithms. * * The following examples show how these policies can be modified. @@ -43,23 +43,26 @@ public class ManagePolicy { @AfterEach public void resetPolicy() { // Policy for hash algorithms in non-revocation signatures - PGPainless.getPolicy().setSignatureHashAlgorithmPolicy( - Policy.HashAlgorithmPolicy.defaultSignatureAlgorithmPolicy()); + PGPainless.getPolicy().setCertificationSignatureHashAlgorithmPolicy( + Policy.HashAlgorithmPolicy.static2022SignatureHashAlgorithmPolicy()); + // Policy for hash algorithms in data signatures + PGPainless.getPolicy().setDataSignatureHashAlgorithmPolicy( + Policy.HashAlgorithmPolicy.static2022SignatureHashAlgorithmPolicy()); // Policy for hash algorithms in revocation signatures PGPainless.getPolicy().setRevocationSignatureHashAlgorithmPolicy( - Policy.HashAlgorithmPolicy.defaultRevocationSignatureHashAlgorithmPolicy()); + Policy.HashAlgorithmPolicy.static2022RevocationSignatureHashAlgorithmPolicy()); // Policy for public key algorithms and bit lengths PGPainless.getPolicy().setPublicKeyAlgorithmPolicy( - Policy.PublicKeyAlgorithmPolicy.defaultPublicKeyAlgorithmPolicy()); + Policy.PublicKeyAlgorithmPolicy.bsi2021PublicKeyAlgorithmPolicy()); // Policy for acceptable symmetric encryption algorithms when decrypting messages PGPainless.getPolicy().setSymmetricKeyDecryptionAlgorithmPolicy( - Policy.SymmetricKeyAlgorithmPolicy.defaultSymmetricKeyDecryptionAlgorithmPolicy()); + Policy.SymmetricKeyAlgorithmPolicy.symmetricKeyDecryptionPolicy2022()); // Policy for acceptable symmetric encryption algorithms when encrypting messages PGPainless.getPolicy().setSymmetricKeyEncryptionAlgorithmPolicy( - Policy.SymmetricKeyAlgorithmPolicy.defaultSymmetricKeyEncryptionAlgorithmPolicy()); + Policy.SymmetricKeyAlgorithmPolicy.symmetricKeyEncryptionPolicy2022()); // Policy for acceptable compression algorithms PGPainless.getPolicy().setCompressionAlgorithmPolicy( - Policy.CompressionAlgorithmPolicy.defaultCompressionAlgorithmPolicy()); + Policy.CompressionAlgorithmPolicy.anyCompressionAlgorithmPolicy()); // Known notations PGPainless.getPolicy().getNotationRegistry().clear(); } @@ -73,7 +76,7 @@ public class ManagePolicy { * * Per default, PGPainless will reject non-revocation signatures that use SHA-1 as hash algorithm. * To inspect PGPainless' default signature hash algorithm policy, see - * {@link Policy.HashAlgorithmPolicy#defaultSignatureAlgorithmPolicy()}. + * {@link Policy.HashAlgorithmPolicy#static2022SignatureHashAlgorithmPolicy()}. * * Since it may be a valid use-case to accept signatures made using SHA-1 as part of a less strict policy, * this example demonstrates how to set a custom signature hash algorithm policy. @@ -83,12 +86,11 @@ public class ManagePolicy { // Get PGPainless' policy singleton Policy policy = PGPainless.getPolicy(); - Policy.HashAlgorithmPolicy sigHashAlgoPolicy = policy.getSignatureHashAlgorithmPolicy(); + Policy.HashAlgorithmPolicy sigHashAlgoPolicy = policy.getDataSignatureHashAlgorithmPolicy(); assertTrue(sigHashAlgoPolicy.isAcceptable(HashAlgorithm.SHA512)); // Per default, non-revocation signatures using SHA-1 are rejected assertFalse(sigHashAlgoPolicy.isAcceptable(HashAlgorithm.SHA1)); - // Create a new custom policy which contains SHA-1 Policy.HashAlgorithmPolicy customPolicy = new Policy.HashAlgorithmPolicy( // The default hash algorithm will be used when hash algorithm negotiation fails when creating a sig @@ -96,10 +98,9 @@ public class ManagePolicy { // List of acceptable hash algorithms Arrays.asList(HashAlgorithm.SHA512, HashAlgorithm.SHA384, HashAlgorithm.SHA256, HashAlgorithm.SHA224, HashAlgorithm.SHA1)); // Set the hash algo policy as policy for non-revocation signatures - policy.setSignatureHashAlgorithmPolicy(customPolicy); + policy.setDataSignatureHashAlgorithmPolicy(customPolicy); - - sigHashAlgoPolicy = policy.getSignatureHashAlgorithmPolicy(); + sigHashAlgoPolicy = policy.getDataSignatureHashAlgorithmPolicy(); assertTrue(sigHashAlgoPolicy.isAcceptable(HashAlgorithm.SHA512)); // SHA-1 is now acceptable as well assertTrue(sigHashAlgoPolicy.isAcceptable(HashAlgorithm.SHA1)); @@ -108,7 +109,8 @@ public class ManagePolicy { /** * Similar to hash algorithms, {@link PublicKeyAlgorithm PublicKeyAlgorithms} tend to get outdated eventually. * Per default, PGPainless will reject signatures made by keys of unacceptable algorithm or length. - * See {@link Policy.PublicKeyAlgorithmPolicy#defaultPublicKeyAlgorithmPolicy()} to inspect PGPainless' defaults. + * See {@link Policy.PublicKeyAlgorithmPolicy#bsi2021PublicKeyAlgorithmPolicy()} + * to inspect PGPainless' defaults. * * This example demonstrates how to set a custom public key algorithm policy. */ @@ -121,7 +123,6 @@ public class ManagePolicy { assertFalse(pkAlgorithmPolicy.isAcceptable(PublicKeyAlgorithm.RSA_GENERAL, 1024)); assertTrue(pkAlgorithmPolicy.isAcceptable(PublicKeyAlgorithm.ECDSA, 256)); - Policy.PublicKeyAlgorithmPolicy customPolicy = new Policy.PublicKeyAlgorithmPolicy( new HashMap(){{ // Put minimum bit strengths for acceptable algorithms. @@ -132,7 +133,6 @@ public class ManagePolicy { ); policy.setPublicKeyAlgorithmPolicy(customPolicy); - pkAlgorithmPolicy = policy.getPublicKeyAlgorithmPolicy(); assertTrue(pkAlgorithmPolicy.isAcceptable(PublicKeyAlgorithm.RSA_GENERAL, 4096)); // RSA 2048 is no longer acceptable @@ -155,10 +155,8 @@ public class ManagePolicy { NotationRegistry notationRegistry = policy.getNotationRegistry(); assertFalse(notationRegistry.isKnownNotation("unknown@pgpainless.org")); - notationRegistry.addKnownNotation("unknown@pgpainless.org"); - assertTrue(notationRegistry.isKnownNotation("unknown@pgpainless.org")); } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/example/ModifyKeys.java b/pgpainless-core/src/test/java/org/pgpainless/example/ModifyKeys.java index 6c5daff8..768064e7 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/example/ModifyKeys.java +++ b/pgpainless-core/src/test/java/org/pgpainless/example/ModifyKeys.java @@ -6,7 +6,6 @@ package org.pgpainless.example; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -26,7 +25,6 @@ import org.pgpainless.PGPainless; import org.pgpainless.algorithm.EncryptionPurpose; import org.pgpainless.algorithm.KeyFlag; import org.pgpainless.exception.WrongPassphraseException; -import org.pgpainless.key.OpenPgpV4Fingerprint; import org.pgpainless.key.generation.KeySpec; import org.pgpainless.key.generation.type.KeyType; import org.pgpainless.key.generation.type.ecc.EllipticCurve; @@ -50,13 +48,14 @@ public class ModifyKeys { private long signingSubkeyId; @BeforeEach - public void generateKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + public void generateKey() + throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { secretKey = PGPainless.generateKeyRing() .modernKeyRing(userId, originalPassphrase); KeyRingInfo info = PGPainless.inspectKeyRing(secretKey); primaryKeyId = info.getKeyId(); - encryptionSubkeyId = info.getEncryptionSubkeys(EncryptionPurpose.STORAGE_AND_COMMUNICATIONS).get(0).getKeyID(); + encryptionSubkeyId = info.getEncryptionSubkeys(EncryptionPurpose.ANY).get(0).getKeyID(); signingSubkeyId = info.getSigningSubkeys().get(0).getKeyID(); } @@ -68,7 +67,6 @@ public class ModifyKeys { // the certificate consists of only the public keys PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKey); - KeyRingInfo info = PGPainless.inspectKeyRing(certificate); assertFalse(info.isSecretKey()); } @@ -80,19 +78,15 @@ public class ModifyKeys { public void toAsciiArmoredString() throws IOException { PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKey); - String asciiArmoredSecretKey = PGPainless.asciiArmor(secretKey); String asciiArmoredCertificate = PGPainless.asciiArmor(certificate); - assertTrue(asciiArmoredSecretKey.startsWith("-----BEGIN PGP PRIVATE KEY BLOCK-----")); assertTrue(asciiArmoredCertificate.startsWith("-----BEGIN PGP PUBLIC KEY BLOCK-----")); } /** * This example demonstrates how to change the passphrase of a secret key and all its subkeys. - * - * @throws PGPException */ @Test public void changePassphrase() throws PGPException { @@ -102,7 +96,6 @@ public class ModifyKeys { .toNewPassphrase(Passphrase.fromPassword("n3wP4ssW0rD")) .done(); - // Old passphrase no longer works assertThrows(WrongPassphraseException.class, () -> UnlockSecretKey.unlockSecretKey(secretKey.getSecretKey(), Passphrase.fromPassword(originalPassphrase))); @@ -113,8 +106,6 @@ public class ModifyKeys { /** * This example demonstrates how to change the passphrase of a single subkey in a key to a new passphrase. * Only the passphrase of the targeted key will be changed. All other keys remain untouched. - * - * @throws PGPException */ @Test public void changeSingleSubkeyPassphrase() throws PGPException { @@ -125,28 +116,28 @@ public class ModifyKeys { .toNewPassphrase(Passphrase.fromPassword("cryptP4ssphr4s3")) .done(); - // encryption key can now only be unlocked using the new passphrase assertThrows(WrongPassphraseException.class, () -> - UnlockSecretKey.unlockSecretKey(secretKey.getSecretKey(encryptionSubkeyId), Passphrase.fromPassword(originalPassphrase))); - UnlockSecretKey.unlockSecretKey(secretKey.getSecretKey(encryptionSubkeyId), Passphrase.fromPassword("cryptP4ssphr4s3")); + UnlockSecretKey.unlockSecretKey( + secretKey.getSecretKey(encryptionSubkeyId), Passphrase.fromPassword(originalPassphrase))); + UnlockSecretKey.unlockSecretKey( + secretKey.getSecretKey(encryptionSubkeyId), Passphrase.fromPassword("cryptP4ssphr4s3")); // primary key remains unchanged - UnlockSecretKey.unlockSecretKey(secretKey.getSecretKey(primaryKeyId), Passphrase.fromPassword(originalPassphrase)); + UnlockSecretKey.unlockSecretKey( + secretKey.getSecretKey(primaryKeyId), Passphrase.fromPassword(originalPassphrase)); } /** * This example demonstrates how to add an additional user-id to a key. - * - * @throws PGPException */ @Test public void addUserId() throws PGPException { - SecretKeyRingProtector protector = SecretKeyRingProtector.unlockAllKeysWith(Passphrase.fromPassword(originalPassphrase), secretKey); + SecretKeyRingProtector protector = + SecretKeyRingProtector.unlockEachKeyWith(Passphrase.fromPassword(originalPassphrase), secretKey); secretKey = PGPainless.modifyKeyRing(secretKey) .addUserId("additional@user.id", protector) .done(); - KeyRingInfo info = PGPainless.inspectKeyRing(secretKey); assertTrue(info.isUserIdValid("additional@user.id")); assertFalse(info.isUserIdValid("another@user.id")); @@ -157,22 +148,19 @@ public class ModifyKeys { * Prerequisites are a {@link SecretKeyRingProtector} that is capable of unlocking the primary key of the existing key, * and a {@link Passphrase} for the new subkey. * - * There are two way to add a subkey into an existing key; + * There are two ways to add a subkey into an existing key; * Either the subkey gets generated on the fly (see below), * or the subkey already exists. In the latter case, the user has to provide * {@link org.bouncycastle.openpgp.PGPSignatureSubpacketVector PGPSignatureSubpacketVectors} for the binding signature * manually. * * Once the subkey is added, it can be decrypted using the provided subkey passphrase. - * - * @throws PGPException - * @throws InvalidAlgorithmParameterException - * @throws NoSuchAlgorithmException */ @Test - public void addSubkey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + public void addSubkey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { // Protector for unlocking the existing secret key - SecretKeyRingProtector protector = SecretKeyRingProtector.unlockAllKeysWith(Passphrase.fromPassword(originalPassphrase), secretKey); + SecretKeyRingProtector protector = + SecretKeyRingProtector.unlockEachKeyWith(Passphrase.fromPassword(originalPassphrase), secretKey); Passphrase subkeyPassphrase = Passphrase.fromPassword("subk3yP4ssphr4s3"); secretKey = PGPainless.modifyKeyRing(secretKey) .addSubKey( @@ -182,7 +170,6 @@ public class ModifyKeys { protector) .done(); - KeyRingInfo info = PGPainless.inspectKeyRing(secretKey); assertEquals(4, info.getSecretKeys().size()); assertEquals(4, info.getPublicKeys().size()); @@ -194,60 +181,32 @@ public class ModifyKeys { /** * This example demonstrates how to set a key expiration date. * The provided expiration date will be set on each user-id certification signature. - * - * @throws PGPException */ @Test public void setKeyExpirationDate() throws PGPException { Date expirationDate = DateUtil.parseUTCDate("2030-06-24 12:44:56 UTC"); SecretKeyRingProtector protector = SecretKeyRingProtector - .unlockAllKeysWith(Passphrase.fromPassword(originalPassphrase), secretKey); + .unlockEachKeyWith(Passphrase.fromPassword(originalPassphrase), secretKey); secretKey = PGPainless.modifyKeyRing(secretKey) .setExpirationDate(expirationDate, protector) .done(); - KeyRingInfo info = PGPainless.inspectKeyRing(secretKey); - assertEquals(DateUtil.formatUTCDate(expirationDate), DateUtil.formatUTCDate(info.getPrimaryKeyExpirationDate())); - assertEquals(DateUtil.formatUTCDate(expirationDate), DateUtil.formatUTCDate(info.getExpirationDateForUse(KeyFlag.ENCRYPT_COMMS))); - assertEquals(DateUtil.formatUTCDate(expirationDate), DateUtil.formatUTCDate(info.getExpirationDateForUse(KeyFlag.SIGN_DATA))); - } - - /** - * This example demonstrates how to set an expiration date for single subkeys. - * - * @throws PGPException - */ - @Test - public void setSubkeyExpirationDate() throws PGPException { - Date expirationDate = DateUtil.parseUTCDate("2032-01-13 22:30:01 UTC"); - SecretKeyRingProtector protector = SecretKeyRingProtector - .unlockAllKeysWith(Passphrase.fromPassword(originalPassphrase), secretKey); - - secretKey = PGPainless.modifyKeyRing(secretKey) - .setExpirationDate( - new OpenPgpV4Fingerprint(secretKey.getPublicKey(encryptionSubkeyId)), - expirationDate, - protector - ) - .done(); - - - KeyRingInfo info = PGPainless.inspectKeyRing(secretKey); - assertNull(info.getPrimaryKeyExpirationDate()); - assertNull(info.getExpirationDateForUse(KeyFlag.SIGN_DATA)); - assertEquals(DateUtil.formatUTCDate(expirationDate), DateUtil.formatUTCDate(info.getExpirationDateForUse(KeyFlag.ENCRYPT_COMMS))); + assertEquals(DateUtil.formatUTCDate(expirationDate), + DateUtil.formatUTCDate(info.getPrimaryKeyExpirationDate())); + assertEquals(DateUtil.formatUTCDate(expirationDate), + DateUtil.formatUTCDate(info.getExpirationDateForUse(KeyFlag.ENCRYPT_COMMS))); + assertEquals(DateUtil.formatUTCDate(expirationDate), + DateUtil.formatUTCDate(info.getExpirationDateForUse(KeyFlag.SIGN_DATA))); } /** * This example demonstrates how to revoke a user-id on a key. - * - * @throws PGPException */ @Test public void revokeUserId() throws PGPException { - SecretKeyRingProtector protector = SecretKeyRingProtector.unlockAllKeysWith( + SecretKeyRingProtector protector = SecretKeyRingProtector.unlockEachKeyWith( Passphrase.fromPassword(originalPassphrase), secretKey); secretKey = PGPainless.modifyKeyRing(secretKey) .addUserId("alcie@pgpainless.org", protector) diff --git a/pgpainless-core/src/test/java/org/pgpainless/example/ReadKeys.java b/pgpainless-core/src/test/java/org/pgpainless/example/ReadKeys.java index ca819233..54e00c26 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/example/ReadKeys.java +++ b/pgpainless-core/src/test/java/org/pgpainless/example/ReadKeys.java @@ -14,6 +14,7 @@ import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; +import org.pgpainless.key.OpenPgpFingerprint; import org.pgpainless.key.OpenPgpV4Fingerprint; import org.pgpainless.key.info.KeyRingInfo; @@ -21,8 +22,6 @@ public class ReadKeys { /** * This example demonstrates how to parse a public key (certificate) from an ASCII armored string. - * - * @throws IOException */ @Test public void readCertificate() throws IOException { @@ -45,21 +44,17 @@ public class ReadKeys { PGPPublicKeyRing publicKey = PGPainless.readKeyRing() .publicKeyRing(certificate); - KeyRingInfo keyInfo = new KeyRingInfo(publicKey); - OpenPgpV4Fingerprint fingerprint = new OpenPgpV4Fingerprint("EB85 BB5F A33A 75E1 5E94 4E63 F231 550C 4F47 E38E"); + OpenPgpFingerprint fingerprint = new OpenPgpV4Fingerprint("EB85 BB5F A33A 75E1 5E94 4E63 F231 550C 4F47 E38E"); assertEquals(fingerprint, keyInfo.getFingerprint()); assertEquals("Alice Lovelace ", keyInfo.getPrimaryUserId()); } /** * This example demonstrates how to parse an ASCII armored secret key. - * - * @throws PGPException - * @throws IOException */ @Test - public void readSecretKey() throws PGPException, IOException { + public void readSecretKey() throws IOException { String key = "\n" + "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + "Comment: Alice's OpenPGP Transferable Secret Key\n" + @@ -81,9 +76,8 @@ public class ReadKeys { PGPSecretKeyRing secretKey = PGPainless.readKeyRing() .secretKeyRing(key); - KeyRingInfo keyInfo = new KeyRingInfo(secretKey); - OpenPgpV4Fingerprint fingerprint = new OpenPgpV4Fingerprint("EB85 BB5F A33A 75E1 5E94 4E63 F231 550C 4F47 E38E"); + OpenPgpFingerprint fingerprint = new OpenPgpV4Fingerprint("EB85 BB5F A33A 75E1 5E94 4E63 F231 550C 4F47 E38E"); assertEquals(fingerprint, keyInfo.getFingerprint()); assertEquals("Alice Lovelace ", keyInfo.getPrimaryUserId()); } @@ -92,10 +86,7 @@ public class ReadKeys { * This example demonstrates how to read a collection of multiple OpenPGP public keys (certificates) at once. * * Note, that a public key collection can both be a concatenation of public key blocks (like below), - * as well as a single public key block containing multiple public key packets. - * - * @throws PGPException - * @throws IOException + * and a single public key block containing multiple public key packets. */ @Test public void readKeyRingCollection() throws PGPException, IOException { diff --git a/pgpainless-core/src/test/java/org/pgpainless/example/Sign.java b/pgpainless-core/src/test/java/org/pgpainless/example/Sign.java index ad3fc0d4..06228c75 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/example/Sign.java +++ b/pgpainless-core/src/test/java/org/pgpainless/example/Sign.java @@ -39,16 +39,13 @@ public class Sign { @BeforeAll public static void prepare() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - secretKey = PGPainless.generateKeyRing().modernKeyRing("Emilia Example ", null); + secretKey = PGPainless.generateKeyRing().modernKeyRing("Emilia Example "); protector = SecretKeyRingProtector.unprotectedKeys(); // no password } /** * Demonstration of how to use the PGPainless API to sign some message using inband signatures. * The result is not human-readable, however the resulting text contains both the signed data and the signatures. - * - * @throws PGPException - * @throws IOException */ @Test public void inbandSignedMessage() throws PGPException, IOException { @@ -58,7 +55,7 @@ public class Sign { EncryptionStream signingStream = PGPainless.encryptAndOrSign() .onOutputStream(signedOut) .withOptions(ProducerOptions.sign(SigningOptions.get() - .addInlineSignature(protector, secretKey, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT)) + .addSignature(protector, secretKey)) ); Streams.pipeAll(messageIn, signingStream); @@ -75,9 +72,6 @@ public class Sign { * A detached signature can be distributed alongside the message/file itself. * * The message/file doesn't need to be altered for detached signature creation. - * - * @throws PGPException - * @throws IOException */ @Test public void detachedSignedMessage() throws PGPException, IOException { @@ -113,9 +107,6 @@ public class Sign { * Demonstration of how to sign a text message in a way that keeps the message content * human-readable by utilizing the OpenPGP Cleartext Signature Framework. * The resulting message contains the original (dash-escaped) message and the signatures. - * - * @throws PGPException - * @throws IOException */ @Test public void cleartextSignedMessage() throws PGPException, IOException { diff --git a/pgpainless-core/src/test/java/org/pgpainless/example/UnlockSecretKeys.java b/pgpainless-core/src/test/java/org/pgpainless/example/UnlockSecretKeys.java index 784cff61..92387978 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/example/UnlockSecretKeys.java +++ b/pgpainless-core/src/test/java/org/pgpainless/example/UnlockSecretKeys.java @@ -11,7 +11,6 @@ import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; -import org.pgpainless.exception.WrongPassphraseException; import org.pgpainless.key.OpenPgpV4Fingerprint; import org.pgpainless.key.TestKeys; import org.pgpainless.key.protection.CachingSecretKeyRingProtector; @@ -34,9 +33,6 @@ public class UnlockSecretKeys { /** * This example demonstrates how to create a {@link SecretKeyRingProtector} for unprotected secret keys. - * - * @throws PGPException - * @throws IOException */ @Test public void unlockUnprotectedKeys() throws PGPException, IOException { @@ -44,24 +40,20 @@ public class UnlockSecretKeys { // This protector will only unlock unprotected keys SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); - assertProtectorUnlocksAllSecretKeys(unprotectedKey, protector); } /** * This example demonstrates how to create a {@link SecretKeyRingProtector} using a single passphrase to unlock * all secret subkeys of a key. - * - * @throws PGPException - * @throws IOException */ @Test public void unlockWholeKeyWithSamePassphrase() throws PGPException, IOException { PGPSecretKeyRing secretKey = TestKeys.getCryptieSecretKeyRing(); - // Unlock all subkeys in the secret key with the same passphrase - SecretKeyRingProtector protector = SecretKeyRingProtector.unlockAllKeysWith( - Passphrase.fromPassword(TestKeys.CRYPTIE_PASSWORD), secretKey); + Passphrase passphrase = TestKeys.CRYPTIE_PASSPHRASE; + // Unlock all subkeys in the secret key with the same passphrase + SecretKeyRingProtector protector = SecretKeyRingProtector.unlockAnyKeyWith(passphrase); assertProtectorUnlocksAllSecretKeys(secretKey, protector); } @@ -69,9 +61,6 @@ public class UnlockSecretKeys { /** * This example demonstrates how to create a {@link SecretKeyRingProtector} that uses different * passphrases per subkey to unlock the secret keys. - * - * @throws PGPException - * @throws IOException */ @Test public void unlockWithPerSubkeyPassphrases() throws PGPException, IOException { @@ -115,12 +104,11 @@ public class UnlockSecretKeys { protector.addPassphrase(new OpenPgpV4Fingerprint("DD8E1195E4B1720E7FB10EF7F60402708E75D941"), Passphrase.fromPassword("s3c0ndsubk3y")); - assertProtectorUnlocksAllSecretKeys(secretKey, protector); } private void assertProtectorUnlocksAllSecretKeys(PGPSecretKeyRing secretKey, SecretKeyRingProtector protector) - throws WrongPassphraseException { + throws PGPException { for (PGPSecretKey key : secretKey) { UnlockSecretKey.unlockSecretKey(key, protector); } diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/BouncycastleExportSubkeys.java b/pgpainless-core/src/test/java/org/pgpainless/key/BouncycastleExportSubkeys.java index f3e40825..f8ea991b 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/BouncycastleExportSubkeys.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/BouncycastleExportSubkeys.java @@ -29,17 +29,16 @@ import org.bouncycastle.openpgp.operator.PGPDigestCalculator; import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder; import org.bouncycastle.openpgp.operator.jcajce.JcaPGPDigestCalculatorProviderBuilder; import org.bouncycastle.openpgp.operator.jcajce.JcaPGPKeyPair; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; -import org.pgpainless.implementation.ImplementationFactory; +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; import org.pgpainless.provider.ProviderFactory; +import org.pgpainless.util.TestAllImplementations; public class BouncycastleExportSubkeys { - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void testExportImport(ImplementationFactory implementationFactory) throws NoSuchAlgorithmException, InvalidAlgorithmParameterException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void testExportImport() throws NoSuchAlgorithmException, InvalidAlgorithmParameterException, PGPException { KeyPairGenerator generator; KeyPair pair; diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/ImportExportKeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/ImportExportKeyTest.java index 0a0d6294..52dac288 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/ImportExportKeyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/ImportExportKeyTest.java @@ -12,11 +12,11 @@ import java.io.IOException; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; +import org.bouncycastle.openpgp.operator.KeyFingerPrintCalculator; +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; import org.pgpainless.implementation.ImplementationFactory; +import org.pgpainless.util.TestAllImplementations; public class ImportExportKeyTest { @@ -24,23 +24,25 @@ public class ImportExportKeyTest { * Test the export and import of a key ring with sub keys. * @throws IOException in case of a IO error */ - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void testExportImportPublicKeyRing(ImplementationFactory implementationFactory) throws IOException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void testExportImportPublicKeyRing() throws IOException { PGPPublicKeyRing publicKeys = TestKeys.getJulietPublicKeyRing(); - BcKeyFingerprintCalculator calc = new BcKeyFingerprintCalculator(); + KeyFingerPrintCalculator calc = ImplementationFactory.getInstance().getKeyFingerprintCalculator(); byte[] bytes = publicKeys.getEncoded(); PGPPublicKeyRing parsed = new PGPPublicKeyRing(bytes, calc); assertArrayEquals(publicKeys.getEncoded(), parsed.getEncoded()); } - @Test + @TestTemplate + @ExtendWith(TestAllImplementations.class) public void testExportImportSecretKeyRing() throws IOException, PGPException { PGPSecretKeyRing secretKeys = TestKeys.getRomeoSecretKeyRing(); + + KeyFingerPrintCalculator calc = ImplementationFactory.getInstance().getKeyFingerprintCalculator(); byte[] bytes = secretKeys.getEncoded(); - PGPSecretKeyRing parsed = new PGPSecretKeyRing(bytes, new BcKeyFingerprintCalculator()); + PGPSecretKeyRing parsed = new PGPSecretKeyRing(bytes, calc); assertArrayEquals(secretKeys.getEncoded(), parsed.getEncoded()); assertEquals(secretKeys.getPublicKey().getKeyID(), parsed.getPublicKey().getKeyID()); } diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/KeyRingValidatorTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/KeyRingValidatorTest.java deleted file mode 100644 index 970cea9c..00000000 --- a/pgpainless-core/src/test/java/org/pgpainless/key/KeyRingValidatorTest.java +++ /dev/null @@ -1,324 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.io.IOException; -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; -import java.util.Date; -import java.util.Iterator; -import java.util.Random; - -import org.bouncycastle.bcpg.attr.ImageAttribute; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPrivateKey; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.PGPSignatureGenerator; -import org.bouncycastle.openpgp.PGPUserAttributeSubpacketVector; -import org.bouncycastle.openpgp.PGPUserAttributeSubpacketVectorGenerator; -import org.junit.jupiter.api.Test; -import org.pgpainless.PGPainless; -import org.pgpainless.algorithm.HashAlgorithm; -import org.pgpainless.algorithm.SignatureType; -import org.pgpainless.implementation.ImplementationFactory; -import org.pgpainless.key.info.KeyRingInfo; -import org.pgpainless.key.protection.SecretKeyRingProtector; -import org.pgpainless.key.protection.UnlockSecretKey; -import org.pgpainless.util.ArmorUtils; -import org.pgpainless.util.CollectionUtils; -import org.pgpainless.util.DateUtil; - -public class KeyRingValidatorTest { - - @Test - public void testRevokedSubkey() throws IOException { - String key = "-----BEGIN PGP ARMORED FILE-----\n" + - "Comment: ASCII Armor added by openpgp-interoperability-test-suite\n" + - "\n" + - "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\n" + - "Y87zSryajDMFXQS0exdvhN4AXDlPlB3Rrkj7CQ==\n" + - "=yTKS\n" + - "-----END PGP ARMORED FILE-----\n"; - - PGPPublicKeyRing keyRing = PGPainless.readKeyRing().publicKeyRing(key); - PGPPublicKeyRing validated = KeyRingValidator.validate(keyRing, PGPainless.getPolicy()); - - Iterator keys = validated.getPublicKeys(); - assertFalse(keys.next().hasRevocation()); - assertTrue(keys.next().hasRevocation()); - } - - @Test - public void badSignatureTest() throws IOException { - String key = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + - "\n" + - "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\n" + - "bVrj5KjhX2PVNEJd3XZRzaXZE2Z/MQ==\n" + - "=6+l9\n" + - "-----END PGP PUBLIC KEY BLOCK-----\n"; - PGPPublicKeyRing publicKeys = PGPainless.readKeyRing().publicKeyRing(key); - PGPPublicKeyRing validated = KeyRingValidator.validate(publicKeys, PGPainless.getPolicy()); - // CHECKSTYLE:OFF - System.out.println(ArmorUtils.toAsciiArmoredString(validated)); - // CHECKSTYLE:ON - } - - @Test - public void unboundSubkey() throws IOException { - String key = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + - "\n" + - "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\n" + - "=T4QR\n" + - "-----END PGP PUBLIC KEY BLOCK-----\n"; - - PGPPublicKeyRing publicKeys = PGPainless.readKeyRing().publicKeyRing(key); - PGPPublicKey unbound = CollectionUtils.iteratorToList(publicKeys.getPublicKeys()).get(2); - assertNotNull(unbound); - - Date validationDate = DateUtil.parseUTCDate("2019-10-15 10:18:26 UTC"); - KeyRingInfo info = new KeyRingInfo(publicKeys, validationDate); - for (PGPPublicKey publicKey : publicKeys) { - if (publicKey != unbound) { - assertTrue(info.isKeyValidlyBound(publicKey.getKeyID())); - } else { - assertFalse(info.isKeyValidlyBound(publicKey.getKeyID())); - } - } - } - - @Test - public void expired() throws IOException { - String key = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + - "\n" + - "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\n" + - "=LxAY\n" + - "-----END PGP PUBLIC KEY BLOCK-----\n"; - - PGPPublicKeyRing publicKeys = PGPainless.readKeyRing().publicKeyRing(key); - PGPPublicKeyRing validated = KeyRingValidator.validate(publicKeys, PGPainless.getPolicy()); - } - - @Test - public void testKeyWithUserAttributes() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() - .modernKeyRing("Alice ", null); - PGPPublicKeyRing publicKeys = PGPainless.extractCertificate(secretKeys); - PGPPublicKey publicKey = secretKeys.getPublicKey(); - PGPSecretKey secretKey = secretKeys.getSecretKey(); - PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKey, SecretKeyRingProtector.unprotectedKeys()); - - PGPSignatureGenerator signatureGenerator = new PGPSignatureGenerator( - ImplementationFactory.getInstance().getPGPContentSignerBuilder(publicKey.getAlgorithm(), HashAlgorithm.SHA512.getAlgorithmId()) - ); - - signatureGenerator.init(SignatureType.CASUAL_CERTIFICATION.getCode(), privateKey); - PGPUserAttributeSubpacketVectorGenerator userAttrGen = new PGPUserAttributeSubpacketVectorGenerator(); - byte[] image = new byte[100]; - new Random().nextBytes(image); - userAttrGen.setImageAttribute(ImageAttribute.JPEG, image); - PGPUserAttributeSubpacketVector userAttr = userAttrGen.generate(); - - PGPSignature certification = signatureGenerator.generateCertification(userAttr, publicKey); - publicKey = PGPPublicKey.addCertification(publicKey, userAttr, certification); - publicKeys = PGPPublicKeyRing.insertPublicKey(publicKeys, publicKey); - secretKeys = PGPSecretKeyRing.replacePublicKeys(secretKeys, publicKeys); - - secretKeys = KeyRingValidator.validate(secretKeys, PGPainless.getPolicy()); - assertTrue(secretKeys.getPublicKey().getUserAttributes().hasNext()); - } -} diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/OpenPgpV4FingerprintTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/OpenPgpV4FingerprintTest.java index 6450e506..43a3dfba 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/OpenPgpV4FingerprintTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/OpenPgpV4FingerprintTest.java @@ -4,15 +4,18 @@ package org.pgpainless.key; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.util.encoders.Hex; import org.junit.jupiter.api.Test; @@ -91,4 +94,54 @@ public class OpenPgpV4FingerprintTest { OpenPgpV4Fingerprint fingerprint = new OpenPgpV4Fingerprint(prettyPrint); assertEquals(prettyPrint, fingerprint.prettyPrint()); } + + @Test + public void testParse() { + String prettyPrint = "C94B 884B 9A56 7B1C FB23 6999 7DC5 BDAC BBDF BF87"; + OpenPgpFingerprint parsed = OpenPgpFingerprint.parse(prettyPrint); + + assertTrue(parsed instanceof OpenPgpV4Fingerprint); + OpenPgpV4Fingerprint v4fp = (OpenPgpV4Fingerprint) parsed; + assertEquals(prettyPrint, v4fp.prettyPrint()); + } + + @Test + public void testParseFromBinary() { + String hex = "5448452043414B452049532041204C4945212121"; + byte[] binary = Hex.decode(hex); + + OpenPgpFingerprint fingerprint = OpenPgpFingerprint.parseFromBinary(binary); + assertTrue(fingerprint instanceof OpenPgpV4Fingerprint); + assertEquals(hex, fingerprint.toString()); + assertArrayEquals(binary, fingerprint.getBytes()); + } + + @Test + public void testParseFromBinary_leadingZeros() { + String hex = "00000000000000000049532041204C4945212121"; + byte[] binary = Hex.decode(hex); + + OpenPgpFingerprint fingerprint = OpenPgpFingerprint.parseFromBinary(binary); + assertTrue(fingerprint instanceof OpenPgpV4Fingerprint); + assertEquals(hex, fingerprint.toString()); + assertArrayEquals(binary, fingerprint.getBytes()); + } + + @Test + public void testParseFromBinary_trailingZeros() { + String hex = "49532041204C4945212121000000000000000000"; + byte[] binary = Hex.decode(hex); + + OpenPgpFingerprint fingerprint = OpenPgpFingerprint.parseFromBinary(binary); + assertTrue(fingerprint instanceof OpenPgpV4Fingerprint); + assertEquals(hex, fingerprint.toString()); + } + + @Test + public void testParseFromBinary_wrongLength() { + String hex = "5448452043414B452049532041204C49452121"; // 2 missing digits + byte[] binary = Hex.decode(hex); + + assertThrows(IllegalArgumentException.class, () -> OpenPgpFingerprint.parseFromBinary(binary)); + } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/OpenPgpV5FingerprintTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/OpenPgpV5FingerprintTest.java new file mode 100644 index 00000000..57c98928 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/key/OpenPgpV5FingerprintTest.java @@ -0,0 +1,112 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.bouncycastle.openpgp.PGPKeyRing; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.util.encoders.Hex; +import org.junit.jupiter.api.Test; + +public class OpenPgpV5FingerprintTest { + + @Test + public void testFingerprintFormatting() { + String pretty = "76543210 ABCDEFAB 01AB23CD 1C0FFEE1 1EEFF0C1 DC32BA10 BAFEDCBA 01234567"; + String fp = pretty.replace(" ", ""); + + OpenPgpV5Fingerprint fingerprint = new OpenPgpV5Fingerprint(fp); + assertEquals(fp, fingerprint.toString()); + assertEquals(pretty, fingerprint.prettyPrint()); + assertEquals(5, fingerprint.getVersion()); + + long id = fingerprint.getKeyId(); + assertEquals("76543210abcdefab", Long.toHexString(id)); + } + + @Test + public void constructFromMockedPublicKey() { + String hex = "76543210ABCDEFAB01AB23CD1C0FFEE11EEFF0C1DC32BA10BAFEDCBA01234567"; + PGPPublicKey publicKey = getMockedPublicKey(hex); + + OpenPgpFingerprint fingerprint = OpenPgpFingerprint.of(publicKey); + assertTrue(fingerprint instanceof OpenPgpV5Fingerprint); + assertEquals(5, fingerprint.getVersion()); + assertEquals(hex, fingerprint.toString()); + } + + @Test + public void constructFromMockedSecretKey() { + String hex = "76543210ABCDEFAB01AB23CD1C0FFEE11EEFF0C1DC32BA10BAFEDCBA01234567"; + PGPPublicKey publicKey = getMockedPublicKey(hex); + PGPSecretKey secretKey = mock(PGPSecretKey.class); + when(secretKey.getPublicKey()).thenReturn(publicKey); + + OpenPgpFingerprint fingerprint = new OpenPgpV5Fingerprint(secretKey); + assertEquals(5, fingerprint.getVersion()); + assertEquals(hex, fingerprint.toString()); + } + + @Test + public void constructFromMockedPublicKeyRing() { + String hex = "76543210ABCDEFAB01AB23CD1C0FFEE11EEFF0C1DC32BA10BAFEDCBA01234567"; + PGPPublicKey publicKey = getMockedPublicKey(hex); + PGPPublicKeyRing publicKeys = mock(PGPPublicKeyRing.class); + when(publicKeys.getPublicKey()).thenReturn(publicKey); + + OpenPgpFingerprint fingerprint = OpenPgpFingerprint.of(publicKeys); + assertEquals(5, fingerprint.getVersion()); + assertEquals(hex, fingerprint.toString()); + + fingerprint = new OpenPgpV5Fingerprint(publicKeys); + assertEquals(hex, fingerprint.toString()); + } + + @Test + public void constructFromMockedSecretKeyRing() { + String hex = "76543210ABCDEFAB01AB23CD1C0FFEE11EEFF0C1DC32BA10BAFEDCBA01234567"; + PGPPublicKey publicKey = getMockedPublicKey(hex); + PGPSecretKeyRing secretKeys = mock(PGPSecretKeyRing.class); + when(secretKeys.getPublicKey()).thenReturn(publicKey); + + OpenPgpFingerprint fingerprint = OpenPgpFingerprint.of(secretKeys); + assertEquals(5, fingerprint.getVersion()); + assertEquals(hex, fingerprint.toString()); + + fingerprint = new OpenPgpV5Fingerprint(secretKeys); + assertEquals(hex, fingerprint.toString()); + } + + @Test + public void constructFromMockedKeyRing() { + String hex = "76543210ABCDEFAB01AB23CD1C0FFEE11EEFF0C1DC32BA10BAFEDCBA01234567"; + PGPPublicKey publicKey = getMockedPublicKey(hex); + PGPKeyRing keys = mock(PGPKeyRing.class); + when(keys.getPublicKey()).thenReturn(publicKey); + + OpenPgpFingerprint fingerprint = OpenPgpFingerprint.of(keys); + assertEquals(5, fingerprint.getVersion()); + assertEquals(hex, fingerprint.toString()); + + fingerprint = new OpenPgpV5Fingerprint(keys); + assertEquals(hex, fingerprint.toString()); + } + + private PGPPublicKey getMockedPublicKey(String hex) { + byte[] binary = Hex.decode(hex); + + PGPPublicKey mocked = mock(PGPPublicKey.class); + when(mocked.getVersion()).thenReturn(5); + when(mocked.getFingerprint()).thenReturn(binary); + return mocked; + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/OpenPgpV6FingerprintTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/OpenPgpV6FingerprintTest.java new file mode 100644 index 00000000..60f6ca67 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/key/OpenPgpV6FingerprintTest.java @@ -0,0 +1,227 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key; + +import org.bouncycastle.openpgp.PGPKeyRing; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.util.encoders.Hex; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; + +import java.io.IOException; +import java.util.Iterator; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class OpenPgpV6FingerprintTest { + + @Test + public void testFingerprintFormatting() { + String pretty = "76543210 ABCDEFAB 01AB23CD 1C0FFEE1 1EEFF0C1 DC32BA10 BAFEDCBA 01234567"; + String fp = pretty.replace(" ", ""); + + OpenPgpV6Fingerprint fingerprint = new OpenPgpV6Fingerprint(fp); + assertEquals(fp, fingerprint.toString()); + assertEquals(pretty, fingerprint.prettyPrint()); + assertEquals(6, fingerprint.getVersion()); + + long id = fingerprint.getKeyId(); + assertEquals("76543210abcdefab", Long.toHexString(id)); + } + + @Test + public void testParseFromBinary_leadingZeros() { + String hex = "000000000000000001AB23CD1C0FFEE11EEFF0C1DC32BA10BAFEDCBA01234567"; + byte[] binary = Hex.decode(hex); + + OpenPgpFingerprint fingerprint = new OpenPgpV6Fingerprint(binary); + assertEquals(hex, fingerprint.toString()); + } + + @Test + public void testParseFromBinary_trailingZeros() { + String hex = "76543210ABCDEFAB01AB23CD1C0FFEE11EEFF0C1DC32BA100000000000000000"; + byte[] binary = Hex.decode(hex); + + OpenPgpFingerprint fingerprint = new OpenPgpV6Fingerprint(binary); + assertEquals(hex, fingerprint.toString()); + } + + @Test + public void testParseFromBinary_wrongLength() { + String hex = "76543210ABCDEFAB01AB23CD1C0FFEE11EEFF0C1DC32BA10BAFEDCBA012345"; // missing 2 digits + byte[] binary = Hex.decode(hex); + + assertThrows(IllegalArgumentException.class, () -> new OpenPgpV6Fingerprint(binary)); + } + + @Test + public void equalsTest() { + String prettyPrint = "76543210 ABCDEFAB 01AB23CD 1C0FFEE1 1EEFF0C1 DC32BA10 BAFEDCBA 01234567"; + OpenPgpFingerprint parsed = new OpenPgpV6Fingerprint(prettyPrint); + + assertNotEquals(parsed, null); + assertNotEquals(parsed, new Object()); + assertEquals(parsed, parsed.toString()); + + OpenPgpFingerprint parsed2 = new OpenPgpV6Fingerprint(prettyPrint); + assertEquals(parsed.hashCode(), parsed2.hashCode()); + assertEquals(0, parsed.compareTo(parsed2)); + } + + @Test + public void constructFromMockedPublicKey() { + String hex = "76543210ABCDEFAB01AB23CD1C0FFEE11EEFF0C1DC32BA10BAFEDCBA01234567"; + PGPPublicKey publicKey = getMockedPublicKey(hex); + + OpenPgpFingerprint fingerprint = OpenPgpFingerprint.of(publicKey); + assertTrue(fingerprint instanceof OpenPgpV6Fingerprint); + assertEquals(6, fingerprint.getVersion()); + assertEquals(hex, fingerprint.toString()); + } + + @Test + public void constructFromMockedSecretKey() { + String hex = "76543210ABCDEFAB01AB23CD1C0FFEE11EEFF0C1DC32BA10BAFEDCBA01234567"; + PGPPublicKey publicKey = getMockedPublicKey(hex); + PGPSecretKey secretKey = mock(PGPSecretKey.class); + when(secretKey.getPublicKey()).thenReturn(publicKey); + + OpenPgpFingerprint fingerprint = new OpenPgpV6Fingerprint(secretKey); + assertEquals(6, fingerprint.getVersion()); + assertEquals(hex, fingerprint.toString()); + } + + @Test + public void constructFromMockedPublicKeyRing() { + String hex = "76543210ABCDEFAB01AB23CD1C0FFEE11EEFF0C1DC32BA10BAFEDCBA01234567"; + PGPPublicKey publicKey = getMockedPublicKey(hex); + PGPPublicKeyRing publicKeys = mock(PGPPublicKeyRing.class); + when(publicKeys.getPublicKey()).thenReturn(publicKey); + + OpenPgpFingerprint fingerprint = OpenPgpFingerprint.of(publicKeys); + assertEquals(6, fingerprint.getVersion()); + assertEquals(hex, fingerprint.toString()); + + fingerprint = new OpenPgpV6Fingerprint(publicKeys); + assertEquals(hex, fingerprint.toString()); + } + + @Test + public void constructFromMockedSecretKeyRing() { + String hex = "76543210ABCDEFAB01AB23CD1C0FFEE11EEFF0C1DC32BA10BAFEDCBA01234567"; + PGPPublicKey publicKey = getMockedPublicKey(hex); + PGPSecretKeyRing secretKeys = mock(PGPSecretKeyRing.class); + when(secretKeys.getPublicKey()).thenReturn(publicKey); + + OpenPgpFingerprint fingerprint = OpenPgpFingerprint.of(secretKeys); + assertEquals(6, fingerprint.getVersion()); + assertEquals(hex, fingerprint.toString()); + + fingerprint = new OpenPgpV6Fingerprint(secretKeys); + assertEquals(hex, fingerprint.toString()); + } + + @Test + public void constructFromMockedKeyRing() { + String hex = "76543210ABCDEFAB01AB23CD1C0FFEE11EEFF0C1DC32BA10BAFEDCBA01234567"; + PGPPublicKey publicKey = getMockedPublicKey(hex); + PGPKeyRing keys = mock(PGPKeyRing.class); + when(keys.getPublicKey()).thenReturn(publicKey); + + OpenPgpFingerprint fingerprint = OpenPgpFingerprint.of(keys); + assertEquals(6, fingerprint.getVersion()); + assertEquals(hex, fingerprint.toString()); + + fingerprint = new OpenPgpV6Fingerprint(keys); + assertEquals(hex, fingerprint.toString()); + } + + @Test + public void fromSampleV6Certificate() throws IOException { + String CERT = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "\n" + + "xioGY4d/4xsAAAAg+U2nu0jWCmHlZ3BqZYfQMxmZu52JGggkLq2EVD34laPCsQYf\n" + + "GwoAAABCBYJjh3/jAwsJBwUVCg4IDAIWAAKbAwIeCSIhBssYbE8GCaaX5NUt+mxy\n" + + "KwwfHifBilZwj2Ul7Ce62azJBScJAgcCAAAAAK0oIBA+LX0ifsDm185Ecds2v8lw\n" + + "gyU2kCcUmKfvBXbAf6rhRYWzuQOwEn7E/aLwIwRaLsdry0+VcallHhSu4RN6HWaE\n" + + "QsiPlR4zxP/TP7mhfVEe7XWPxtnMUMtf15OyA51YBM4qBmOHf+MZAAAAIIaTJINn\n" + + "+eUBXbki+PSAld2nhJh/LVmFsS+60WyvXkQ1wpsGGBsKAAAALAWCY4d/4wKbDCIh\n" + + "BssYbE8GCaaX5NUt+mxyKwwfHifBilZwj2Ul7Ce62azJAAAAAAQBIKbpGG2dWTX8\n" + + "j+VjFM21J0hqWlEg+bdiojWnKfA5AQpWUWtnNwDEM0g12vYxoWM8Y81W+bHBw805\n" + + "I8kWVkXU6vFOi+HWvv/ira7ofJu16NnoUkhclkUrk0mXubZvyl4GBg==\n" + + "-----END PGP PUBLIC KEY BLOCK-----"; + PGPPublicKeyRing cert = PGPainless.readKeyRing().publicKeyRing(CERT); + assertNotNull(cert); + + OpenPgpFingerprint fingerprint = OpenPgpFingerprint.of(cert); + assertEquals(6, fingerprint.getVersion()); + assertEquals("CB186C4F0609A697E4D52DFA6C722B0C1F1E27C18A56708F6525EC27BAD9ACC9", fingerprint.toString()); + + Iterator keys = cert.getPublicKeys(); + fingerprint = OpenPgpFingerprint.of(keys.next()); + assertEquals(6, fingerprint.getVersion()); + assertEquals("CB186C4F0609A697E4D52DFA6C722B0C1F1E27C18A56708F6525EC27BAD9ACC9", + fingerprint.toString()); + + fingerprint = OpenPgpFingerprint.of(keys.next()); + assertEquals(6, fingerprint.getVersion()); + assertEquals("12C83F1E706F6308FE151A417743A1F033790E93E9978488D1DB378DA9930885", + fingerprint.toString()); + } + + @Test + public void fromSampleV6SecretKey() throws IOException { + String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "\n" + + "xUsGY4d/4xsAAAAg+U2nu0jWCmHlZ3BqZYfQMxmZu52JGggkLq2EVD34laMAGXKB\n" + + "exK+cH6NX1hs5hNhIB00TrJmosgv3mg1ditlsLfCsQYfGwoAAABCBYJjh3/jAwsJ\n" + + "BwUVCg4IDAIWAAKbAwIeCSIhBssYbE8GCaaX5NUt+mxyKwwfHifBilZwj2Ul7Ce6\n" + + "2azJBScJAgcCAAAAAK0oIBA+LX0ifsDm185Ecds2v8lwgyU2kCcUmKfvBXbAf6rh\n" + + "RYWzuQOwEn7E/aLwIwRaLsdry0+VcallHhSu4RN6HWaEQsiPlR4zxP/TP7mhfVEe\n" + + "7XWPxtnMUMtf15OyA51YBMdLBmOHf+MZAAAAIIaTJINn+eUBXbki+PSAld2nhJh/\n" + + "LVmFsS+60WyvXkQ1AE1gCk95TUR3XFeibg/u/tVY6a//1q0NWC1X+yui3O24wpsG\n" + + "GBsKAAAALAWCY4d/4wKbDCIhBssYbE8GCaaX5NUt+mxyKwwfHifBilZwj2Ul7Ce6\n" + + "2azJAAAAAAQBIKbpGG2dWTX8j+VjFM21J0hqWlEg+bdiojWnKfA5AQpWUWtnNwDE\n" + + "M0g12vYxoWM8Y81W+bHBw805I8kWVkXU6vFOi+HWvv/ira7ofJu16NnoUkhclkUr\n" + + "k0mXubZvyl4GBg==\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); + assertNotNull(secretKeys); + + OpenPgpFingerprint fingerprint = OpenPgpFingerprint.of(secretKeys); + assertEquals(6, fingerprint.getVersion()); + assertEquals("CB186C4F0609A697E4D52DFA6C722B0C1F1E27C18A56708F6525EC27BAD9ACC9", fingerprint.toString()); + + Iterator keys = secretKeys.getSecretKeys(); + fingerprint = OpenPgpFingerprint.of(keys.next()); + assertEquals(6, fingerprint.getVersion()); + assertEquals("CB186C4F0609A697E4D52DFA6C722B0C1F1E27C18A56708F6525EC27BAD9ACC9", + fingerprint.toString()); + + fingerprint = OpenPgpFingerprint.of(keys.next()); + assertEquals(6, fingerprint.getVersion()); + assertEquals("12C83F1E706F6308FE151A417743A1F033790E93E9978488D1DB378DA9930885", + fingerprint.toString()); + } + + private PGPPublicKey getMockedPublicKey(String hex) { + byte[] binary = Hex.decode(hex); + + PGPPublicKey mocked = mock(PGPPublicKey.class); + when(mocked.getVersion()).thenReturn(6); + when(mocked.getFingerprint()).thenReturn(binary); + return mocked; + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/SelfCertifyingRevocationTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/SelfCertifyingRevocationTest.java new file mode 100644 index 00000000..5923f352 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/key/SelfCertifyingRevocationTest.java @@ -0,0 +1,179 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.key.info.KeyRingInfo; +import org.pgpainless.util.CollectionUtils; + +/** + * Test for v6-style self-certifying revocations. + * @see + * OpenPGP v6 Revocation Certificate + */ +public class SelfCertifyingRevocationTest { + + private static final String SECRET_KEY = "" + + "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 8139 D35E 40CB 1A6E 0E0F B769 7FD2 60B1 95C2 A78F\n" + + "Comment: Alice \n" + + "\n" + + "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\n" + + "sI1nYaqkdT6UC4PRnl3P\n" + + "=isPD\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + + private static final String SELF_CERTIFYING_REVOCATION = "" + + "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 8139 D35E 40CB 1A6E 0E0F B769 7FD2 60B1 95C2 A78F\n" + + "\n" + + "mQGNBGSIp4gBDADhFvBqIHyyYdyhiXH0UZNDDIcKY3g3KLRFN0DZU1LNUkm1E9Cc\n" + + "y81hyvEFDrSQGyn8bKmTQF/P3RzU4WTyQerwSmegycsOVr6c9b/s+Pz+EWaP7hHS\n" + + "RN2Hr83GUcVRXZO4Q6wFJItkiJxgbigb7Xlkd45/synVqcLuDQNUvREITKvqLtTI\n" + + "ZIWoRI5RYUqr4qIX3zDGd8c2/6W2z1LwFpiK5j68q40mDsK/z0lei3Up9SWho3Vk\n" + + "qPOcWVPWQBoxOtfFTNeZntuL48KicEnwFB5HOXMbo98/j1mhk/je6HedxkkKIndw\n" + + "1T7//eqi/+fVOBkGTzLby1ZaDVZXKEU7Sg8WhLPAD/s30PhUR8s7av3rj+ZbJcho\n" + + "L7TeC80ndQw0OHbHnCTAZFYpeZZq93lAm544CrRlkzSQyjpyvfViHcWcB37iWZld\n" + + "TV1Y7mwM4h7Lcc7U6ZvgDFzNWImol6G3vpSGtSdaD3ZUsBQsyJKa3kczB+r0MGJw\n" + + "XgmCufWgj/sB+zMAEQEAAYkBtgQgAQoAKgUCZIinigkQf9JgsZXCp48WIQSBOdNe\n" + + "QMsabg4Pt2l/0mCxlcKnjwKHAAAA35EL/3Qubi7A9rkFxcz+YF2AoLOqRJA4p5VA\n" + + "oxUGQSIyOvRKBgoZJugvM330DP2shJmJWSgsZqt/obXkZNbPCdjQ0hXR+Ih1/tdh\n" + + "Mf4lCD9vPx9V88xFo37BYZCnaEk93NTDGrJ6sjvo9GQkm9x2Rlrzek//jmQ2wqvV\n" + + "GviYDBADhWzuJzyibdo1Re/4ESRlGpzN0bFf0j1/uSvEBq41Yq3UXXZqg/NX01id\n" + + "t4P0dxGYfQTXwzTxyYxn7rbdFJUAc5uxoGyDZzzVgoXwgNu8aE92faztALajagxF\n" + + "P3NpfT+TlZ51hsNZcIz8tnQFUyYLNeAycYI4iCC/Vd5RPhK8t9Py9Sow2Tk4Pdf4\n" + + "5l8MSIBsrtHiL/ElwFlnMP5Ffwuqfhnb/29yK0tWVpbRkE199tvU3C8iggyhdZYC\n" + + "/8NvYSU/RysdDwgDH1j1WWxuFTnaRCpzP/jPZ9sA6NFQtbOYjN6FLO+78gDZbuDv\n" + + "Il9nFzjaEHbjZ7QVqINZxSCQe8NYmjfeDQ==\n" + + "=zlRL\n" + + "-----END PGP PUBLIC KEY BLOCK-----"; + + @Test + public void parseSelfCertifyingRevocation() throws IOException { + PGPPublicKeyRing revocation = PGPainless.readKeyRing().publicKeyRing(SELF_CERTIFYING_REVOCATION); + assertNotNull(revocation); + assertEquals(1, CollectionUtils.iteratorToList(revocation.getPublicKeys()).size()); + } + + @Test + public void selfCertifyingRevocationIsRevoked() throws IOException { + PGPPublicKeyRing revocation = PGPainless.readKeyRing().publicKeyRing(SELF_CERTIFYING_REVOCATION); + assertNotNull(revocation); + KeyRingInfo info = PGPainless.inspectKeyRing(revocation); + assertTrue(info.getRevocationState().isHardRevocation()); + } + + @Test + public void mergeCertificatesResultsInRevokedKey() throws IOException, PGPException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(SECRET_KEY); + assertNotNull(secretKeys); + PGPPublicKeyRing publicKeys = PGPainless.extractCertificate(secretKeys); + KeyRingInfo info = PGPainless.inspectKeyRing(publicKeys); + assertTrue(info.getRevocationState().isNotRevoked()); + + PGPPublicKeyRing revocation = PGPainless.readKeyRing().publicKeyRing(SELF_CERTIFYING_REVOCATION); + assertNotNull(revocation); + + PGPPublicKeyRing merged = PGPainless.mergeCertificate(publicKeys, revocation); + info = PGPainless.inspectKeyRing(merged); + assertTrue(info.getRevocationState().isHardRevocation()); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/SubkeyIdentifierTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/SubkeyIdentifierTest.java index fe792e4d..add18fd5 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/SubkeyIdentifierTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/SubkeyIdentifierTest.java @@ -5,8 +5,10 @@ package org.pgpainless.key; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.IOException; import java.util.NoSuchElementException; @@ -102,6 +104,15 @@ public class SubkeyIdentifierTest { assertNotEquals(id1, null); } + @Test + public void testIsPrimaryKey() { + SubkeyIdentifier primaryKey = new SubkeyIdentifier(PRIMARY_FP); + assertTrue(primaryKey.isPrimaryKey()); + + SubkeyIdentifier subKey = new SubkeyIdentifier(PRIMARY_FP, SUBKEY_FP); + assertFalse(subKey.isPrimaryKey()); + } + @Test public void nonExistentSubkeyThrowsNoSuchElementException() { assertThrows(NoSuchElementException.class, () -> new SubkeyIdentifier(CERT, 123)); diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/TestMergeCertificate.java b/pgpainless-core/src/test/java/org/pgpainless/key/TestMergeCertificate.java new file mode 100644 index 00000000..34861a5c --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/key/TestMergeCertificate.java @@ -0,0 +1,106 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; +import org.junit.JUtils; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.key.info.KeyRingInfo; +import org.pgpainless.key.util.KeyRingUtils; +import org.pgpainless.signature.SignatureUtils; +import org.pgpainless.util.DateUtil; + +import java.io.IOException; +import java.util.Date; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class TestMergeCertificate { + + private static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 9F3E C7B3 3FCF 807E 516D 5DA1 C102 B0FC 9A1C 69E9\n" + + "Comment: Revik Okemi \n" + + "\n" + + "lFgEYxXwbRYJKwYBBAHaRw8BAQdAtAWpi1+uUUpe37nSQqybiLpcAoa5KhlpLZmk\n" + + "IkqLXn8AAP4s+6jp7OInR4PqasuH0YefMEfPu9ZY5ZHjq3HFoaqEpxTxtBhSZXZp\n" + + "ayBPa2VtaSA8cmV2QG9rZS5taT6IjwQTFgoAQQUCYxXwbQkQwQKw/JocaekWIQSf\n" + + "PsezP8+AflFtXaHBArD8mhxp6QKeAQKbAQUWAgMBAAQLCQgHBRUKCQgLApkBAAAi\n" + + "SAEApd8RdhvF33eiUgXlMBU3/ob1/NdMIbVJCBUXj7URYzUBAKxH+BwesiSagsXO\n" + + "KbQEOjzu1R7Nd2Hmf+gue9AVQQ0BnF0EYxXwbRIKKwYBBAGXVQEFAQEHQPLc0OH8\n" + + "8v+govDgUQs7gnM5NK3H+haFCsq/ILMBb48YAwEIBwAA/2CXgEXUIi4s38GaVbDK\n" + + "ts7nj3CWwEOAqtLsO8+QcXmoEyuIdQQYFgoAHQUCYxXwbQKeAQKbDAUWAgMBAAQL\n" + + "CQgHBRUKCQgLAAoJEMECsPyaHGnpO7AA/2zF7j5cgxCZ+Ws+ENj6Uzgq47kqsRxa\n" + + "Ii4kPjW1HmCtAP4rie2Z0ra/1alG/wu2bUtxHgEkeTBsHP8pOM5Xz4JVDZxYBGMV\n" + + "8G0WCSsGAQQB2kcPAQEHQHofxjdBzpFaLsiyEDRaotbB5/New7vdtAHV7t5rv1BU\n" + + "AAD/fnI4ilbhsRYaGSGX5ma7VfkgWiK7UQi04YpJVV3HOEYO/ojVBBgWCgB9BQJj\n" + + "FfBtAp4BApsCBRYCAwEABAsJCAcFFQoJCAtfIAQZFgoABgUCYxXwbQAKCRCUM1S1\n" + + "VUVbouF5AQDQUJIkFikWriyhSMWEUS52l0i3SlllmPCJuDc1dy389AD9FXCU5+W0\n" + + "GT2N1hRb8eIf+0aDiVLCdV3folVbuPaNvgcACgkQwQKw/Jocaem+GwD+NJD8EIdP\n" + + "Nf4Q3IvT9YFXEbilk+mKw3IdV68DsQxEtQoBAPkugEJxuI2XNEdl6sigtGF94q3u\n" + + "IzX9xT12kqD4GtgO\n" + + "=slQ4\n" + + "-----END PGP PRIVATE KEY BLOCK-----\n"; + + private static final String SOFT_REVOCATION = "-----BEGIN PGP SIGNATURE-----\n" + + "Version: PGPainless\n" + + "\n" + + "iHsEIBYKAC0FAmMV8kcJEMECsPyaHGnpFiEEnz7Hsz/PgH5RbV2hwQKw/JocaekC\n" + + "hwACHQMAAMTqAP9XbUer/yjcAUOpbggqC35zrhzXi4/zc6QuuM9NSLnePwD/YZCn\n" + + "NoE+7B24C/SZVr7d4U0ryB2gNWJdvfMfQnGLaQA=\n" + + "=d2pq\n" + + "-----END PGP SIGNATURE-----"; + private static final Date SOFT_REVOCATION_DATE = DateUtil.parseUTCDate("2022-09-05 12:57:43 UTC"); + + private static final String HARD_REVOCATION = "-----BEGIN PGP SIGNATURE-----\n" + + "Version: PGPainless\n" + + "\n" + + "iHsEIBYKAC0FAmMV8pUJEMECsPyaHGnpFiEEnz7Hsz/PgH5RbV2hwQKw/JocaekC\n" + + "hwACHQIAAFaCAQCZPxqJHe87GqLjaDuMdTPdI1dT8kuHvBC4LfhMP2VobQEAiCgQ\n" + + "WMqWZTfJmbhubnUhEnTu/+qPFiHChgDnaJmoMAk=\n" + + "=pl4A\n" + + "-----END PGP SIGNATURE-----"; + + @Test + public void testRevocationStateWithDifferentRevocationsMerged() throws IOException, PGPException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); + PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKeys); + + KeyRingInfo info = PGPainless.inspectKeyRing(certificate); + assertTrue(info.getRevocationState().isNotRevoked()); + + PGPSignature softRevocation = SignatureUtils.readSignatures(SOFT_REVOCATION).get(0); + PGPPublicKeyRing softRevoked = KeyRingUtils.injectCertification(certificate, softRevocation); + + info = PGPainless.inspectKeyRing(softRevoked, softRevoked.getPublicKey().getCreationTime()); + assertTrue(info.getRevocationState().isNotRevoked(), + "Expect: Cert is not revoked at creation time, although we already added soft revocation"); + + info = KeyRingInfo.evaluateForSignature(softRevoked, softRevocation); + assertTrue(info.getRevocationState().isSoftRevocation(), "Expect: Cert is now revoked, since now is after soft revocation creation"); + JUtils.assertDateEquals(SOFT_REVOCATION_DATE, info.getRevocationDate()); + + PGPSignature hardRevocation = SignatureUtils.readSignatures(HARD_REVOCATION).get(0); + PGPPublicKeyRing hardRevoked = KeyRingUtils.injectCertification(certificate, hardRevocation); + + info = PGPainless.inspectKeyRing(hardRevoked); + assertTrue(info.getRevocationState().isHardRevocation()); + + info = PGPainless.inspectKeyRing(hardRevoked, hardRevoked.getPublicKey().getCreationTime()); + assertTrue(info.getRevocationState().isHardRevocation(), "Expect: Key is hard revoked, no matter reference time"); + + PGPPublicKeyRing merged = PGPainless.mergeCertificate(certificate, softRevoked); + info = PGPainless.inspectKeyRing(merged); + assertTrue(info.getRevocationState().isSoftRevocation()); + + merged = PGPainless.mergeCertificate(merged, hardRevoked); + info = PGPainless.inspectKeyRing(merged); + assertTrue(info.getRevocationState().isHardRevocation()); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java index 4e596b3f..1290ad9c 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java @@ -4,31 +4,17 @@ package org.pgpainless.key; -import org.junit.jupiter.api.Test; -import org.pgpainless.key.util.UserId; - import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.Comparator; + +import org.junit.jupiter.api.Test; +import org.pgpainless.key.util.UserId; public class UserIdTest { - @Test - public void throwForNullName() { - assertThrows(IllegalArgumentException.class, () -> UserId.newBuilder().withName(null)); - } - - @Test - public void throwForNullComment() { - assertThrows(IllegalArgumentException.class, () -> UserId.newBuilder().withComment(null)); - } - - @Test - public void throwForNullEmail() { - assertThrows(IllegalArgumentException.class, () -> UserId.newBuilder().withEmail(null)); - } - @Test public void testFormatOnlyName() { assertEquals( @@ -66,11 +52,6 @@ public class UserIdTest { .toString()); } - @Test - public void throwIfOnlyEmailEmailNull() { - assertThrows(IllegalArgumentException.class, () -> UserId.onlyEmail(null)); - } - @Test public void testNameAndEmail() { UserId userId = UserId.nameAndEmail("Maurice Moss", "moss.m@reynholm.co.uk"); @@ -151,15 +132,13 @@ public class UserIdTest { @Test void testEmailOnlyFormatting() { final UserId userId = UserId.onlyEmail("john.smith@example.com"); - assertEquals("john.smith@example.com", userId.toString()); + assertEquals("", userId.toString()); } @Test void testEmptyNameAndValidEmailFormatting() { final UserId userId = UserId.nameAndEmail("", "john.smith@example.com"); - assertEquals("john.smith@example.com", userId.toString()); - assertEquals("john.smith@example.com", userId.asString(false)); - assertEquals("john.smith@example.com", userId.asString(true)); + assertEquals("", userId.toString()); } @Test @@ -169,9 +148,7 @@ public class UserIdTest { .withName("") .withEmail("john.smith@example.com") .build(); - assertEquals(" () ", userId.toString()); - assertEquals(" () ", userId.asString(false)); - assertEquals("john.smith@example.com", userId.asString(true)); + assertEquals("", userId.toString()); } @Test @@ -206,4 +183,352 @@ public class UserIdTest { final UserId userId2 = UserId.newBuilder().withComment(comment2).withName(name).withEmail(email).build(); assertNotEquals(userId1, userId2); } + + @Test + public void testLength() { + UserId id = UserId.nameAndEmail("Alice", "alice@pgpainless.org"); + assertEquals(28, id.length()); + } + + @Test + public void testSubSequence() { + UserId id = UserId.onlyEmail("alice@pgpainless.org"); + assertEquals("alice@pgpainless.org", id.subSequence(1, id.length() - 1)); + } + + @Test + public void toStringTest() { + UserId id = UserId.builder() + .withName("Alice") + .withComment("Work Email") + .withEmail("alice@pgpainless.org") + .build(); + + assertEquals(id.toString(), id.toString()); + } + + @Test + public void charAtTest() { + UserId id = UserId.onlyEmail("alice@pgpainless.org"); + assertEquals('<', id.charAt(0)); + assertEquals('>', id.charAt(id.length() - 1)); + } + + @Test + public void defaultCompareTest() { + UserId id1 = UserId.onlyEmail("alice@pgpainless.org"); + UserId id2 = UserId.onlyEmail("alice@gnupg.org"); + UserId id3 = UserId.nameAndEmail("Alice", "alice@pgpainless.org"); + UserId id3_ = UserId.nameAndEmail("Alice", "alice@pgpainless.org"); + UserId id4 = UserId.newBuilder().withName("Alice").build(); + UserId id5 = UserId.newBuilder().withName("Alice").withComment("Work Mail").withEmail("alice@pgpainless.org").build(); + + assertEquals(id3.hashCode(), id3_.hashCode()); + assertNotEquals(id2.hashCode(), id3.hashCode()); + + Comparator c = new UserId.DefaultComparator(); + assertEquals(0, UserId.compare(null, null, c)); + assertEquals(0, UserId.compare(id1, id1, c)); + assertNotEquals(0, UserId.compare(id1, null, c)); + assertNotEquals(0, UserId.compare(null, id1, c)); + assertNotEquals(0, UserId.compare(id1, id2, c)); + assertNotEquals(0, UserId.compare(id2, id1, c)); + assertNotEquals(0, UserId.compare(id1, id3, c)); + assertNotEquals(0, UserId.compare(id1, id4, c)); + assertNotEquals(0, UserId.compare(id4, id1, c)); + assertNotEquals(0, UserId.compare(id2, id3, c)); + assertNotEquals(0, UserId.compare(id1, id5, c)); + assertNotEquals(0, UserId.compare(id5, id1, c)); + assertNotEquals(0, UserId.compare(id3, id5, c)); + assertNotEquals(0, UserId.compare(id5, id3, c)); + assertEquals(0, UserId.compare(id3, id3, c)); + assertEquals(0, UserId.compare(id3, id3_, c)); + } + + @Test + public void defaultIgnoreCaseCompareTest() { + UserId id1 = UserId.nameAndEmail("Alice", "alice@pgpainless.org"); + UserId id2 = UserId.nameAndEmail("alice", "alice@pgpainless.org"); + UserId id3 = UserId.nameAndEmail("Alice", "Alice@Pgpainless.Org"); + UserId id4 = UserId.newBuilder().withName("Alice").withComment("Work Email").withEmail("Alice@Pgpainless.Org").build(); + UserId id5 = UserId.newBuilder().withName("alice").withComment("work email").withEmail("alice@pgpainless.org").build(); + UserId id6 = UserId.nameAndEmail("Bob", "bob@pgpainless.org"); + + Comparator c = new UserId.DefaultIgnoreCaseComparator(); + assertEquals(0, UserId.compare(id1, id2, c)); + assertEquals(0, UserId.compare(id1, id3, c)); + assertEquals(0, UserId.compare(id2, id3, c)); + assertEquals(0, UserId.compare(null, null, c)); + assertEquals(0, UserId.compare(id1, id1, c)); + assertEquals(0, UserId.compare(id4, id4, c)); + assertEquals(0, UserId.compare(id4, id5, c)); + assertEquals(0, UserId.compare(id5, id4, c)); + assertNotEquals(0, UserId.compare(null, id1, c)); + assertNotEquals(0, UserId.compare(id1, null, c)); + assertNotEquals(0, UserId.compare(id1, id4, c)); + assertNotEquals(0, UserId.compare(id4, id1, c)); + assertNotEquals(0, UserId.compare(id1, id6, c)); + assertNotEquals(0, UserId.compare(id6, id1, c)); + } + + @Test + public void parseNameAndEmail() { + UserId id = UserId.parse("Alice "); + + assertEquals("Alice", id.getName()); + assertNull(id.getComment()); + assertEquals("alice@pgpainless.org", id.getEmail()); + + assertEquals("Alice ", id.toString()); + } + + @Test + public void parseNameCommentAndEmail() { + UserId id = UserId.parse("Alice (work mail) "); + + assertEquals("Alice", id.getName()); + assertEquals("work mail", id.getComment()); + assertEquals("alice@pgpainless.org", id.getEmail()); + + assertEquals("Alice (work mail) ", id.toString()); + } + + @Test + public void parseLongNameAndEmail() { + UserId id = UserId.parse("Alice von Painleicester "); + + assertEquals("Alice von Painleicester", id.getName()); + assertNull(id.getComment()); + assertEquals("alice@pgpainless.org", id.getEmail()); + + assertEquals("Alice von Painleicester ", id.toString()); + } + + @Test + public void parseLongNameCommentAndEmail() { + UserId id = UserId.parse("Alice von Painleicester (work email) "); + + assertEquals("Alice von Painleicester", id.getName()); + assertEquals("work email", id.getComment()); + assertEquals("alice@pgpainless.org", id.getEmail()); + + assertEquals("Alice von Painleicester (work email) ", id.toString()); + } + + @Test + public void parseQuotedNameAndEmail() { + UserId id = UserId.parse("\"Alice\" "); + + assertEquals("Alice", id.getName()); + assertNull(id.getComment()); + assertEquals("alice@pgpainless.org", id.getEmail()); + + assertEquals("\"Alice\" ", id.toString()); + } + + @Test + public void parseQuotedNameCommentAndEmail() { + UserId id = UserId.parse("\"Alice\" (work email) "); + + assertEquals("Alice", id.getName()); + assertEquals("work email", id.getComment()); + assertEquals("alice@pgpainless.org", id.getEmail()); + + assertEquals("\"Alice\" (work email) ", id.toString()); + } + + @Test + public void parseLongQuotedNameAndEmail() { + UserId id = UserId.parse("\"Alice Mac Painlester\" "); + + assertEquals("Alice Mac Painlester", id.getName()); + assertNull(id.getComment()); + assertEquals("alice@pgpainless.org", id.getEmail()); + + assertEquals("\"Alice Mac Painlester\" ", id.toString()); + } + + @Test + public void parseLongQuotedNameCommentAndEmail() { + UserId id = UserId.parse("\"Alice Mac Painlester\" (work email) "); + + assertEquals("Alice Mac Painlester", id.getName()); + assertEquals("work email", id.getComment()); + assertEquals("alice@pgpainless.org", id.getEmail()); + + assertEquals("\"Alice Mac Painlester\" (work email) ", id.toString()); + } + + @Test + public void parseEmailOnly() { + UserId id = UserId.parse("alice@pgpainless.org"); + + assertNull(id.getName()); + assertNull(id.getComment()); + assertEquals("alice@pgpainless.org", id.getEmail()); + + assertEquals("", id.toString()); + } + + @Test + public void parseBracketedEmailOnly() { + UserId id = UserId.parse(""); + + assertNull(id.getName()); + assertNull(id.getComment()); + assertEquals("alice@pgpainless.org", id.getEmail()); + + assertEquals("", id.toString()); + } + + @Test + public void parseLatinWithDiacritics() { + UserId pele = UserId.parse("Pelé@example.com"); + assertEquals("Pelé@example.com", pele.getEmail()); + + pele = UserId.parse("Marquez Pelé "); + assertEquals("Pelé@example.com", pele.getEmail()); + assertEquals("Marquez Pelé", pele.getName()); + } + + @Test + public void parseGreekAlphabet() { + UserId dokimi = UserId.parse("δοκιμή@παράδειγμα.δοκιμή"); + assertEquals("δοκιμή@παράδειγμα.δοκιμή", dokimi.getEmail()); + + dokimi = UserId.parse("δοκιμή <δοκιμή@παράδειγμα.δοκιμή>"); + assertEquals("δοκιμή", dokimi.getName()); + assertEquals("δοκιμή@παράδειγμα.δοκιμή", dokimi.getEmail()); + } + + @Test + public void parseTraditionalChinese() { + UserId womai = UserId.parse("我買@屋企.香港"); + assertEquals("我買@屋企.香港", womai.getEmail()); + + womai = UserId.parse("我買 <我買@屋企.香港>"); + assertEquals("我買@屋企.香港", womai.getEmail()); + assertEquals("我買", womai.getName()); + } + + @Test + public void parseJapanese() { + UserId ninomiya = UserId.parse("二ノ宮@黒川.日本"); + assertEquals("二ノ宮@黒川.日本", ninomiya.getEmail()); + + ninomiya = UserId.parse("二ノ宮 <二ノ宮@黒川.日本>"); + assertEquals("二ノ宮@黒川.日本", ninomiya.getEmail()); + assertEquals("二ノ宮", ninomiya.getName()); + } + + @Test + public void parseCyrillic() { + UserId medved = UserId.parse("медведь@с-балалайкой.рф"); + assertEquals("медведь@с-балалайкой.рф", medved.getEmail()); + + medved = UserId.parse("медведь <медведь@с-балалайкой.рф>"); + assertEquals("медведь@с-балалайкой.рф", medved.getEmail()); + assertEquals("медведь", medved.getName()); + } + + @Test + public void parseDevanagari() { + UserId samparka = UserId.parse("संपर्क@डाटामेल.भारत"); + assertEquals("संपर्क@डाटामेल.भारत", samparka.getEmail()); + + samparka = UserId.parse("संपर्क <संपर्क@डाटामेल.भारत>"); + assertEquals("संपर्क@डाटामेल.भारत", samparka.getEmail()); + assertEquals("संपर्क", samparka.getName()); + } + + @Test + public void parseMailWithPlus() { + UserId id = UserId.parse("disposable.style.email.with+symbol@example.com"); + assertEquals("disposable.style.email.with+symbol@example.com", id.getEmail()); + + id = UserId.parse("Disposable Mail "); + assertEquals("disposable.style.email.with+symbol@example.com", id.getEmail()); + assertEquals("Disposable Mail", id.getName()); + } + + @Test + public void parseMailWithHyphen() { + UserId id = UserId.parse("other.email-with-hyphen@example.com"); + assertEquals("other.email-with-hyphen@example.com", id.getEmail()); + + id = UserId.parse("Other Email "); + assertEquals("other.email-with-hyphen@example.com", id.getEmail()); + assertEquals("Other Email", id.getName()); + } + + @Test + public void parseMailWithTagAndSorting() { + UserId id = UserId.parse("user.name+tag+sorting@example.com"); + assertEquals("user.name+tag+sorting@example.com", id.getEmail()); + + id = UserId.parse("User Name "); + assertEquals("user.name+tag+sorting@example.com", id.getEmail()); + assertEquals("User Name", id.getName()); + } + + @Test + public void parseMailWithSlash() { + UserId id = UserId.parse("test/test@test.com"); + assertEquals("test/test@test.com", id.getEmail()); + + id = UserId.parse("Who uses Slashes "); + assertEquals("test/test@test.com", id.getEmail()); + assertEquals("Who uses Slashes", id.getName()); + } + + @Test + public void parseDoubleDots() { + UserId id = UserId.parse("\"john..doe\"@example.org"); + assertEquals("\"john..doe\"@example.org", id.getEmail()); + + id = UserId.parse("John Doe <\"john..doe\"@example.org>"); + assertEquals("\"john..doe\"@example.org", id.getEmail()); + assertEquals("John Doe", id.getName()); + } + + @Test + public void parseBangifiedHostRoute() { + UserId id = UserId.parse("mailhost!username@example.org"); + assertEquals("mailhost!username@example.org", id.getEmail()); + + id = UserId.parse("Bangified Host Route "); + assertEquals("mailhost!username@example.org", id.getEmail()); + assertEquals("Bangified Host Route", id.getName()); + } + + @Test + public void parsePercentRouted() { + UserId id = UserId.parse("user%example.com@example.org"); + assertEquals("user%example.com@example.org", id.getEmail()); + + id = UserId.parse("User "); + assertEquals("user%example.com@example.org", id.getEmail()); + assertEquals("User", id.getName()); + } + + @Test + public void parseLocalPartEndingWithNonAlphanumericCharacter() { + UserId id = UserId.parse("user-@example.org"); + assertEquals("user-@example.org", id.getEmail()); + + id = UserId.parse("User "); + assertEquals("user-@example.org", id.getEmail()); + assertEquals("User", id.getName()); + } + + @Test + public void parseDomainIsIpAddress() { + UserId id = UserId.parse("postmaster@[123.123.123.123]"); + assertEquals("postmaster@[123.123.123.123]", id.getEmail()); + + id = UserId.parse("Alice (work email) "); + assertEquals("postmaster@[123.123.123.123]", id.getEmail()); + assertEquals("Alice", id.getName()); + assertEquals("work email", id.getComment()); + } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/V3KeyBehaviorTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/V3KeyBehaviorTest.java new file mode 100644 index 00000000..a1d768ae --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/key/V3KeyBehaviorTest.java @@ -0,0 +1,86 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key; + +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; + +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * V3 keys are not supported by PGPainless. + * However, some basic functions like parsing the keys or converting a secret key to a certificate still work. + */ +public class V3KeyBehaviorTest { + + private static final String V3Cert = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "\n" + + "mQCNA2JqgDIAAAEEAOYdcIKFQ5ZWBx0D5DKwMMNFcIhFyqmfDJ0v23ehMxOkXN/o\n" + + "HO/43+dq6ZqQn0gNw53Tp9no+EmcCYNrZuN0C4Zu8XHSyY6UB+CqzNkz/CwmV10E\n" + + "dRDipcG1O6scJyy2MWpuOG67til+o+wOLgEkkVkSW8Bl2oqtzVVP4swtKLRZAAUR\n" + + "tClKb2huIFEuIFNtaXRoIDwxMjM0NS42Nzg5QGNvbXB1c2VydmUuY29tPokAlQMF\n" + + "EGJqgDJVT+LMLSi0WQEBgiwEALKQnuzza+oIgp7CAukW6qhUaOV/Cf3P4bWhru+v\n" + + "8bED+YUOvgTytnXK1QUxQJ/PLnYV860NBRVR46kCtpZDgl+NeQe4O5lxbZVGHZy1\n" + + "P+FUcbvUaA5ZQEfcR5cBJKcWO9RUTf28SMSyJ1ozFm0yPmOa2J5MwHylIbVAlc9c\n" + + "ag3J\n" + + "=GebS\n" + + "-----END PGP PUBLIC KEY BLOCK-----\n"; + private static final String V3Key = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "\n" + + "lQHYA2JqgDIAAAEEAOYdcIKFQ5ZWBx0D5DKwMMNFcIhFyqmfDJ0v23ehMxOkXN/o\n" + + "HO/43+dq6ZqQn0gNw53Tp9no+EmcCYNrZuN0C4Zu8XHSyY6UB+CqzNkz/CwmV10E\n" + + "dRDipcG1O6scJyy2MWpuOG67til+o+wOLgEkkVkSW8Bl2oqtzVVP4swtKLRZAAUR\n" + + "AAP+JBiyRqt+DYr8GKE85NBX9nlS6DMaxUYgGKgibR5OSVsJjIjNUtG0sNmODjTN\n" + + "sPMZqlNln6wS3l7APMWNoStNGc9JG9Puz3eR2W69lPDzhuxuxrHIUBO+3UlEQB/p\n" + + "N3NPhnwCjh3OWHSMM6rzsX5ExUv0Z4FypnzvMG1x6GRJDVECAO6PyY8NDHsktMVN\n" + + "HAdgC61iIOz+GbLhNGeikuB+DQpSoyckAF0N5reBxRbyjzNZQ7aVvWpxigUp5OdK\n" + + "HMK7YcwTAgD275bcqhd+oWHDhyesi6RVswlqGfix48qahf9wOmDkc0nzp8evy/4V\n" + + "4Qu5zUJGVzi4aEIbFaAnc5lMD9/ydTNjAf485vh4MDFRd3tPvx9mPrHQgaArCBX8\n" + + "9oImPDk0oaKixwSIFzXeg1qZQeLiwv26Fs8gawWsLVZpR4+zZc1nhZlGnrQpSm9o\n" + + "biBRLiBTbWl0aCA8MTIzNDUuNjc4OUBjb21wdXNlcnZlLmNvbT6JAJUDBRBiaoAy\n" + + "VU/izC0otFkBAYIsBACykJ7s82vqCIKewgLpFuqoVGjlfwn9z+G1oa7vr/GxA/mF\n" + + "Dr4E8rZ1ytUFMUCfzy52FfOtDQUVUeOpAraWQ4JfjXkHuDuZcW2VRh2ctT/hVHG7\n" + + "1GgOWUBH3EeXASSnFjvUVE39vEjEsidaMxZtMj5jmtieTMB8pSG1QJXPXGoNyQ==\n" + + "=p7Lr\n" + + "-----END PGP PRIVATE KEY BLOCK-----\n"; + + @Test + public void readV3PublicKey() throws IOException { + PGPPublicKeyRing cert = PGPainless.readKeyRing().publicKeyRing(V3Cert); + assertEquals(3, cert.getPublicKey().getVersion()); + assertEquals("John Q. Smith <12345.6789@compuserve.com>", cert.getPublicKey().getUserIDs().next()); + } + + @Test + public void readV3SecretKey() throws IOException { + PGPSecretKeyRing key = PGPainless.readKeyRing().secretKeyRing(V3Key); + assertEquals(3, key.getPublicKey().getVersion()); + assertEquals("John Q. Smith <12345.6789@compuserve.com>", key.getPublicKey().getUserIDs().next()); + } + + @Test + public void extractV3Cert() throws IOException { + PGPSecretKeyRing key = PGPainless.readKeyRing().secretKeyRing(V3Key); + PGPPublicKeyRing cert = PGPainless.readKeyRing().publicKeyRing(V3Cert); + + PGPPublicKeyRing extractedCert = PGPainless.extractCertificate(key); + assertArrayEquals(cert.getEncoded(), extractedCert.getEncoded()); + } + + @Test + public void v3FingerprintNotSupported() throws IOException { + PGPSecretKeyRing key = PGPainless.readKeyRing().secretKeyRing(V3Key); + assertThrows(IllegalArgumentException.class, () -> OpenPgpFingerprint.of(key)); + + PGPPublicKeyRing cert = PGPainless.readKeyRing().publicKeyRing(V3Cert); + assertThrows(IllegalArgumentException.class, () -> OpenPgpFingerprint.of(cert)); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/V6OpenPgpKeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/V6OpenPgpKeyTest.java new file mode 100644 index 00000000..e30ce77c --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/key/V6OpenPgpKeyTest.java @@ -0,0 +1,58 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key; + +import java.io.IOException; + +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; +import org.pgpainless.PGPainless; +import org.pgpainless.util.TestAllImplementations; + +public class V6OpenPgpKeyTest { + + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-09.html#name-sample-v6-secret-key-transf + private static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "\n" + + "xUsGY4d/4xsAAAAg+U2nu0jWCmHlZ3BqZYfQMxmZu52JGggkLq2EVD34laMAGXKB\n" + + "exK+cH6NX1hs5hNhIB00TrJmosgv3mg1ditlsLfCsQYfGwoAAABCBYJjh3/jAwsJ\n" + + "BwUVCg4IDAIWAAKbAwIeCSIhBssYbE8GCaaX5NUt+mxyKwwfHifBilZwj2Ul7Ce6\n" + + "2azJBScJAgcCAAAAAK0oIBA+LX0ifsDm185Ecds2v8lwgyU2kCcUmKfvBXbAf6rh\n" + + "RYWzuQOwEn7E/aLwIwRaLsdry0+VcallHhSu4RN6HWaEQsiPlR4zxP/TP7mhfVEe\n" + + "7XWPxtnMUMtf15OyA51YBMdLBmOHf+MZAAAAIIaTJINn+eUBXbki+PSAld2nhJh/\n" + + "LVmFsS+60WyvXkQ1AE1gCk95TUR3XFeibg/u/tVY6a//1q0NWC1X+yui3O24wpsG\n" + + "GBsKAAAALAWCY4d/4wKbDCIhBssYbE8GCaaX5NUt+mxyKwwfHifBilZwj2Ul7Ce6\n" + + "2azJAAAAAAQBIKbpGG2dWTX8j+VjFM21J0hqWlEg+bdiojWnKfA5AQpWUWtnNwDE\n" + + "M0g12vYxoWM8Y81W+bHBw805I8kWVkXU6vFOi+HWvv/ira7ofJu16NnoUkhclkUr\n" + + "k0mXubZvyl4GBg==\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-09.html#name-sample-v6-certificate-trans + private static final String CERT = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "\n" + + "xioGY4d/4xsAAAAg+U2nu0jWCmHlZ3BqZYfQMxmZu52JGggkLq2EVD34laPCsQYf\n" + + "GwoAAABCBYJjh3/jAwsJBwUVCg4IDAIWAAKbAwIeCSIhBssYbE8GCaaX5NUt+mxy\n" + + "KwwfHifBilZwj2Ul7Ce62azJBScJAgcCAAAAAK0oIBA+LX0ifsDm185Ecds2v8lw\n" + + "gyU2kCcUmKfvBXbAf6rhRYWzuQOwEn7E/aLwIwRaLsdry0+VcallHhSu4RN6HWaE\n" + + "QsiPlR4zxP/TP7mhfVEe7XWPxtnMUMtf15OyA51YBM4qBmOHf+MZAAAAIIaTJINn\n" + + "+eUBXbki+PSAld2nhJh/LVmFsS+60WyvXkQ1wpsGGBsKAAAALAWCY4d/4wKbDCIh\n" + + "BssYbE8GCaaX5NUt+mxyKwwfHifBilZwj2Ul7Ce62azJAAAAAAQBIKbpGG2dWTX8\n" + + "j+VjFM21J0hqWlEg+bdiojWnKfA5AQpWUWtnNwDEM0g12vYxoWM8Y81W+bHBw805\n" + + "I8kWVkXU6vFOi+HWvv/ira7ofJu16NnoUkhclkUrk0mXubZvyl4GBg==\n" + + "-----END PGP PUBLIC KEY BLOCK-----"; + + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void testParseCert() throws IOException { + PGPPublicKeyRing cert = PGPainless.readKeyRing().publicKeyRing(CERT); + } + + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void testParseKey() throws IOException { + PGPSecretKeyRing key = PGPainless.readKeyRing().secretKeyRing(KEY); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/WeirdKeys.java b/pgpainless-core/src/test/java/org/pgpainless/key/WeirdKeys.java index 87cea724..4b0bb9de 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/WeirdKeys.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/WeirdKeys.java @@ -4,17 +4,38 @@ package org.pgpainless.key; -import java.io.IOException; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.util.Collections; + +import org.bouncycastle.bcpg.HashAlgorithmTags; import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPrivateKey; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.PGPSignatureGenerator; +import org.bouncycastle.util.Strings; +import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.SignatureType; +import org.pgpainless.implementation.ImplementationFactory; +import org.pgpainless.key.info.KeyRingInfo; +import org.pgpainless.key.protection.UnlockSecretKey; +import org.pgpainless.util.Passphrase; /** * This class contains a set of slightly out of spec or weird keys. * Those are used to test whether implementations behave correctly when dealing with such keys. * - * Original source: https://gitlab.com/sequoia-pgp/weird-keys + * @see Original Source */ public class WeirdKeys { @@ -48,7 +69,7 @@ public class WeirdKeys { "=BlPm\n" + "-----END PGP PRIVATE KEY BLOCK-----\n"; - public static PGPSecretKeyRing getTwoCryptSubkeysKey() throws IOException, PGPException { + public static PGPSecretKeyRing getTwoCryptSubkeysKey() throws IOException { return PGPainless.readKeyRing().secretKeyRing(TWO_CRYPT_SUBKEYS); } @@ -77,7 +98,58 @@ public class WeirdKeys { "=h6sT\n" + "-----END PGP PRIVATE KEY BLOCK-----\n"; - public static PGPSecretKeyRing getArchiveCommsSubkeysKey() throws IOException, PGPException { + public static PGPSecretKeyRing getArchiveCommsSubkeysKey() throws IOException { return PGPainless.readKeyRing().secretKeyRing(ARCHIVE_COMMS_SUBKEYS); } + + @Test + public void generateCertAndTestWithNonUTF8UserId() + throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + PGPSecretKeyRing nakedKey = PGPainless.generateKeyRing().modernKeyRing(null); + PGPPublicKey pubKey = nakedKey.getPublicKey(); + PGPSecretKey secKey = nakedKey.getSecretKey(); + PGPPrivateKey privKey = UnlockSecretKey.unlockSecretKey(secKey, Passphrase.emptyPassphrase()); + + // Non-UTF8 User-ID + ByteArrayOutputStream bOut = new ByteArrayOutputStream(); + for (int i = 0xc0; i <= 0xdf; i++) { + bOut.write(i); + bOut.write(" ".getBytes()); + } + byte[] idBytes = bOut.toByteArray(); + + // Check that this is in fact not a valid UTF8 String + assertThrows(IllegalArgumentException.class, () -> Strings.fromUTF8ByteArray(idBytes)); + + PGPSignatureGenerator sigGen = new PGPSignatureGenerator( + ImplementationFactory.getInstance().getPGPContentSignerBuilder( + pubKey.getAlgorithm(), + HashAlgorithmTags.SHA512)); + sigGen.init(SignatureType.GENERIC_CERTIFICATION.getCode(), privKey); + + // We have to manually generate the signature over the user-ID + // updateWithKey() + byte[] keyBytes = pubKey.getPublicKeyPacket().getEncodedContents(); + sigGen.update((byte) 0x99); + sigGen.update((byte) (keyBytes.length >> 8)); + sigGen.update((byte) (keyBytes.length)); + sigGen.update(keyBytes); + + // Update with ID data + sigGen.update((byte) 0xb4); + sigGen.update((byte) (idBytes.length >> 24)); + sigGen.update((byte) (idBytes.length >> 16)); + sigGen.update((byte) (idBytes.length >> 8)); + sigGen.update((byte) (idBytes.length)); + sigGen.update(idBytes); + + PGPSignature signature = sigGen.generate(); + pubKey = PGPPublicKey.addCertification(pubKey, idBytes, signature); + + PGPPublicKeyRing cert = new PGPPublicKeyRing(Collections.singletonList(pubKey)); + + // This might fail + KeyRingInfo info = PGPainless.inspectKeyRing(cert); + assertTrue(info.getUserIds().isEmpty()); // Malformed ID is ignored + } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/_64DigitFingerprintTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/_64DigitFingerprintTest.java new file mode 100644 index 00000000..a38fa61d --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/key/_64DigitFingerprintTest.java @@ -0,0 +1,89 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key; + +import org.bouncycastle.util.encoders.Hex; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class _64DigitFingerprintTest { + + @Test + public void testParse() { + String prettyPrint = "76543210 ABCDEFAB 01AB23CD 1C0FFEE1 1EEFF0C1 DC32BA10 BAFEDCBA 01234567"; + OpenPgpFingerprint parsed = OpenPgpFingerprint.parse(prettyPrint); + + assertTrue(parsed instanceof _64DigitFingerprint); + assertEquals(prettyPrint, parsed.prettyPrint()); + assertEquals(-1, parsed.getVersion()); + } + + @Test + public void testParseFromBinary() { + String hex = "76543210ABCDEFAB01AB23CD1C0FFEE11EEFF0C1DC32BA10BAFEDCBA01234567"; + byte[] binary = Hex.decode(hex); + + OpenPgpFingerprint fingerprint = OpenPgpFingerprint.parseFromBinary(binary); + assertTrue(fingerprint instanceof _64DigitFingerprint); + assertEquals(hex, fingerprint.toString()); + + OpenPgpV5Fingerprint v5 = new OpenPgpV5Fingerprint(binary); + assertEquals(fingerprint, v5); + + OpenPgpV6Fingerprint v6 = new OpenPgpV6Fingerprint(binary); + assertEquals(fingerprint, v6); + } + + @Test + public void testParseFromBinary_leadingZeros() { + String hex = "000000000000000001AB23CD1C0FFEE11EEFF0C1DC32BA10BAFEDCBA01234567"; + byte[] binary = Hex.decode(hex); + + OpenPgpFingerprint fingerprint = OpenPgpFingerprint.parseFromBinary(binary); + assertTrue(fingerprint instanceof _64DigitFingerprint); + assertEquals(hex, fingerprint.toString()); + } + + @Test + public void testParseFromBinary_trailingZeros() { + String hex = "76543210ABCDEFAB01AB23CD1C0FFEE11EEFF0C1DC32BA100000000000000000"; + byte[] binary = Hex.decode(hex); + + OpenPgpFingerprint fingerprint = OpenPgpFingerprint.parseFromBinary(binary); + assertTrue(fingerprint instanceof _64DigitFingerprint); + assertEquals(hex, fingerprint.toString()); + } + + @Test + public void testParseFromBinary_wrongLength() { + String hex = "76543210ABCDEFAB01AB23CD1C0FFEE11EEFF0C1DC32BA10BAFEDCBA012345"; // missing 2 digits + byte[] binary = Hex.decode(hex); + + assertThrows(IllegalArgumentException.class, () -> OpenPgpFingerprint.parseFromBinary(binary)); + } + + @Test + public void equalsTest() { + String prettyPrint = "76543210 ABCDEFAB 01AB23CD 1C0FFEE1 1EEFF0C1 DC32BA10 BAFEDCBA 01234567"; + OpenPgpFingerprint parsed = OpenPgpFingerprint.parse(prettyPrint); + + assertNotEquals(parsed, null); + assertNotEquals(parsed, new Object()); + assertEquals(parsed, parsed.toString()); + + OpenPgpFingerprint v5 = new OpenPgpV5Fingerprint(prettyPrint); + assertEquals(parsed.hashCode(), v5.hashCode()); + assertEquals(0, parsed.compareTo(v5)); + + OpenPgpFingerprint v6 = new OpenPgpV6Fingerprint(prettyPrint); + assertEquals(parsed.hashCode(), v6.hashCode()); + assertEquals(0, parsed.compareTo(v6)); + } + +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/certification/CertifyCertificateTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/certification/CertifyCertificateTest.java new file mode 100644 index 00000000..eb0069f5 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/key/certification/CertifyCertificateTest.java @@ -0,0 +1,165 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.certification; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.util.List; + +import org.bouncycastle.bcpg.sig.TrustSignature; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.util.Arrays; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.CertificationType; +import org.pgpainless.algorithm.SignatureType; +import org.pgpainless.algorithm.Trustworthiness; +import org.pgpainless.key.info.KeyRingInfo; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.signature.consumer.SignatureVerifier; +import org.pgpainless.signature.subpackets.CertificationSubpackets; +import org.pgpainless.util.CollectionUtils; +import org.pgpainless.util.DateUtil; + +public class CertifyCertificateTest { + + @Test + public void testUserIdCertification() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); + PGPSecretKeyRing alice = PGPainless.generateKeyRing().modernKeyRing("Alice "); + String bobUserId = "Bob "; + PGPSecretKeyRing bob = PGPainless.generateKeyRing().modernKeyRing(bobUserId); + + PGPPublicKeyRing bobCertificate = PGPainless.extractCertificate(bob); + + CertifyCertificate.CertificationResult result = PGPainless.certify() + .userIdOnCertificate(bobUserId, bobCertificate) + .withKey(alice, protector) + .build(); + + assertNotNull(result); + PGPSignature signature = result.getCertification(); + assertNotNull(signature); + assertEquals(SignatureType.GENERIC_CERTIFICATION, SignatureType.valueOf(signature.getSignatureType())); + assertEquals(alice.getPublicKey().getKeyID(), signature.getKeyID()); + + assertTrue(SignatureVerifier.verifyUserIdCertification( + bobUserId, signature, alice.getPublicKey(), bob.getPublicKey(), PGPainless.getPolicy(), DateUtil.now())); + + PGPPublicKeyRing bobCertified = result.getCertifiedCertificate(); + PGPPublicKey bobCertifiedKey = bobCertified.getPublicKey(); + // There are 2 sigs now, bobs own and alice' + assertEquals(2, CollectionUtils.iteratorToList(bobCertifiedKey.getSignaturesForID(bobUserId)).size()); + List sigsByAlice = CollectionUtils.iteratorToList( + bobCertifiedKey.getSignaturesForKeyID(alice.getPublicKey().getKeyID())); + assertEquals(1, sigsByAlice.size()); + assertEquals(signature, sigsByAlice.get(0)); + + assertFalse(Arrays.areEqual(bobCertificate.getEncoded(), bobCertified.getEncoded())); + } + + @Test + public void testKeyDelegation() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); + PGPSecretKeyRing alice = PGPainless.generateKeyRing().modernKeyRing("Alice "); + PGPSecretKeyRing bob = PGPainless.generateKeyRing().modernKeyRing("Bob "); + + PGPPublicKeyRing bobCertificate = PGPainless.extractCertificate(bob); + + CertifyCertificate.CertificationResult result = PGPainless.certify() + .certificate(bobCertificate, Trustworthiness.fullyTrusted().introducer()) + .withKey(alice, protector) + .build(); + + assertNotNull(result); + PGPSignature signature = result.getCertification(); + assertNotNull(signature); + assertEquals(SignatureType.DIRECT_KEY, SignatureType.valueOf(signature.getSignatureType())); + assertEquals(alice.getPublicKey().getKeyID(), signature.getKeyID()); + TrustSignature trustSignaturePacket = signature.getHashedSubPackets().getTrust(); + assertNotNull(trustSignaturePacket); + Trustworthiness trustworthiness = new Trustworthiness(trustSignaturePacket.getTrustAmount(), trustSignaturePacket.getDepth()); + assertTrue(trustworthiness.isFullyTrusted()); + assertTrue(trustworthiness.isIntroducer()); + assertFalse(trustworthiness.canIntroduce(1)); + + assertTrue(SignatureVerifier.verifyDirectKeySignature( + signature, alice.getPublicKey(), bob.getPublicKey(), PGPainless.getPolicy(), DateUtil.now())); + + PGPPublicKeyRing bobCertified = result.getCertifiedCertificate(); + PGPPublicKey bobCertifiedKey = bobCertified.getPublicKey(); + + List sigsByAlice = CollectionUtils.iteratorToList( + bobCertifiedKey.getSignaturesForKeyID(alice.getPublicKey().getKeyID())); + assertEquals(1, sigsByAlice.size()); + assertEquals(signature, sigsByAlice.get(0)); + + assertFalse(Arrays.areEqual(bobCertificate.getEncoded(), bobCertified.getEncoded())); + } + + @Test + public void testPetNameCertification() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + PGPSecretKeyRing aliceKey = PGPainless.generateKeyRing() + .modernKeyRing("Alice "); + PGPSecretKeyRing bobKey = PGPainless.generateKeyRing() + .modernKeyRing("Bob "); + + PGPPublicKeyRing bobCert = PGPainless.extractCertificate(bobKey); + String petName = "Bobby"; + + CertifyCertificate.CertificationResult result = PGPainless.certify() + .userIdOnCertificate(petName, bobCert) + .withKey(aliceKey, SecretKeyRingProtector.unprotectedKeys()) + .buildWithSubpackets(new CertificationSubpackets.Callback() { + @Override + public void modifyHashedSubpackets(CertificationSubpackets hashedSubpackets) { + hashedSubpackets.setExportable(false); + } + }); + + PGPSignature certification = result.getCertification(); + assertEquals(aliceKey.getPublicKey().getKeyID(), certification.getKeyID()); + assertEquals(CertificationType.GENERIC.asSignatureType().getCode(), certification.getSignatureType()); + + PGPPublicKeyRing certWithPetName = result.getCertifiedCertificate(); + KeyRingInfo info = PGPainless.inspectKeyRing(certWithPetName); + assertTrue(info.getUserIds().contains(petName)); + assertFalse(info.getValidUserIds().contains(petName)); + } + + @Test + public void testScopedDelegation() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + PGPSecretKeyRing aliceKey = PGPainless.generateKeyRing() + .modernKeyRing("Alice "); + PGPSecretKeyRing caKey = PGPainless.generateKeyRing() + .modernKeyRing("CA "); + PGPPublicKeyRing caCert = PGPainless.extractCertificate(caKey); + + CertifyCertificate.CertificationResult result = PGPainless.certify() + .certificate(caCert, Trustworthiness.fullyTrusted().introducer()) + .withKey(aliceKey, SecretKeyRingProtector.unprotectedKeys()) + .buildWithSubpackets(new CertificationSubpackets.Callback() { + @Override + public void modifyHashedSubpackets(CertificationSubpackets hashedSubpackets) { + hashedSubpackets.setRegularExpression("^.*<.+@example.com>.*$"); + } + }); + + PGPSignature certification = result.getCertification(); + assertEquals(SignatureType.DIRECT_KEY.getCode(), certification.getSignatureType()); + assertEquals("^.*<.+@example.com>.*$", + certification.getHashedSubPackets().getRegularExpression().getRegex()); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/collection/PGPKeyRingCollectionTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/collection/PGPKeyRingCollectionTest.java index 5a5a8471..fd5530ba 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/collection/PGPKeyRingCollectionTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/collection/PGPKeyRingCollectionTest.java @@ -52,18 +52,18 @@ public class PGPKeyRingCollectionTest { // silent = true -> No exception, but not keys either PGPKeyRingCollection collection = new PGPKeyRingCollection(bytes, true); assertEquals(0, collection.getPgpPublicKeyRingCollection().size()); - assertEquals(0, collection.getPGPSecretKeyRingCollection().size()); + assertEquals(0, collection.getPgpSecretKeyRingCollection().size()); } @Test - public void testConstructorFromCollection() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + public void testConstructorFromCollection() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { PGPSecretKeyRing first = PGPainless.generateKeyRing().simpleEcKeyRing("alice@wonderland.lit"); PGPSecretKeyRing second = PGPainless.generateKeyRing().simpleEcKeyRing("bob@the-builder.tv"); PGPPublicKeyRing secondPub = KeyRingUtils.publicKeyRingFrom(second); Collection keys = Arrays.asList(first, second, secondPub); PGPKeyRingCollection collection = new PGPKeyRingCollection(keys, true); - assertEquals(2, collection.getPGPSecretKeyRingCollection().size()); + assertEquals(2, collection.getPgpSecretKeyRingCollection().size()); assertEquals(1, collection.getPgpPublicKeyRingCollection().size()); } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/generation/BrainpoolKeyGenerationTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/generation/BrainpoolKeyGenerationTest.java index feed47f6..77023908 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/generation/BrainpoolKeyGenerationTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/generation/BrainpoolKeyGenerationTest.java @@ -17,29 +17,27 @@ import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.KeyFlag; import org.pgpainless.algorithm.PublicKeyAlgorithm; -import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.generation.type.KeyType; import org.pgpainless.key.generation.type.ecc.EllipticCurve; -import org.pgpainless.key.generation.type.eddsa.EdDSACurve; +import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve; import org.pgpainless.key.generation.type.rsa.RsaLength; -import org.pgpainless.key.generation.type.xdh.XDHSpec; +import org.pgpainless.key.generation.type.xdh_legacy.XDHLegacySpec; import org.pgpainless.key.info.KeyInfo; import org.pgpainless.key.util.UserId; -import org.pgpainless.util.BCUtil; import org.pgpainless.util.Passphrase; +import org.pgpainless.util.TestAllImplementations; public class BrainpoolKeyGenerationTest { - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void generateEcKeysTest(ImplementationFactory implementationFactory) + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void generateEcKeysTest() throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); for (EllipticCurve curve : EllipticCurve.values()) { PGPSecretKeyRing secretKeys = generateKey( @@ -65,18 +63,17 @@ public class BrainpoolKeyGenerationTest { } } - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void generateEdDSAKeyTest(ImplementationFactory implementationFactory) + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void generateEdDSAKeyTest() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - ImplementationFactory.setFactoryImplementation(implementationFactory); - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() + PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() .setPrimaryKey(KeySpec.getBuilder( KeyType.ECDSA(EllipticCurve._BRAINPOOLP384R1), KeyFlag.CERTIFY_OTHER)) - .addSubkey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.SIGN_DATA)) + .addSubkey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.SIGN_DATA)) .addSubkey(KeySpec.getBuilder( - KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)) + KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)) .addSubkey(KeySpec.getBuilder( KeyType.RSA(RsaLength._3072), KeyFlag.SIGN_DATA)) .addUserId(UserId.nameAndEmail("Alice", "alice@pgpainless.org")) @@ -98,26 +95,26 @@ public class BrainpoolKeyGenerationTest { PGPSecretKey ecdsaPrim = iterator.next(); KeyInfo ecdsaInfo = new KeyInfo(ecdsaPrim); assertEquals(EllipticCurve._BRAINPOOLP384R1.getName(), ecdsaInfo.getCurveName()); - assertEquals(384, BCUtil.getBitStrength(ecdsaPrim.getPublicKey())); + assertEquals(384, ecdsaPrim.getPublicKey().getBitStrength()); PGPSecretKey eddsaSub = iterator.next(); KeyInfo eddsaInfo = new KeyInfo(eddsaSub); - assertEquals(EdDSACurve._Ed25519.getName(), eddsaInfo.getCurveName()); - assertEquals(256, BCUtil.getBitStrength(eddsaSub.getPublicKey())); + assertEquals(EdDSALegacyCurve._Ed25519.getName(), eddsaInfo.getCurveName()); + assertEquals(256, eddsaSub.getPublicKey().getBitStrength()); PGPSecretKey xdhSub = iterator.next(); KeyInfo xdhInfo = new KeyInfo(xdhSub); - assertEquals(XDHSpec._X25519.getCurveName(), xdhInfo.getCurveName()); - assertEquals(256, BCUtil.getBitStrength(xdhSub.getPublicKey())); + assertEquals(XDHLegacySpec._X25519.getCurveName(), xdhInfo.getCurveName()); + assertEquals(256, xdhSub.getPublicKey().getBitStrength()); PGPSecretKey rsaSub = iterator.next(); KeyInfo rsaInfo = new KeyInfo(rsaSub); assertThrows(IllegalArgumentException.class, rsaInfo::getCurveName, "RSA is not a curve-based encryption system"); - assertEquals(3072, BCUtil.getBitStrength(rsaSub.getPublicKey())); + assertEquals(3072, rsaSub.getPublicKey().getBitStrength()); } public PGPSecretKeyRing generateKey(KeySpec primaryKey, KeySpec subKey, String userId) throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() + PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() .setPrimaryKey(primaryKey) .addSubkey(subKey) .addUserId(userId) diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/generation/CertificationKeyMustBeAbleToCertifyTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/generation/CertificationKeyMustBeAbleToCertifyTest.java index 7b9747dc..02d5aed0 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/generation/CertificationKeyMustBeAbleToCertifyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/generation/CertificationKeyMustBeAbleToCertifyTest.java @@ -6,14 +6,14 @@ package org.pgpainless.key.generation; import static org.junit.jupiter.api.Assertions.assertThrows; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.KeyFlag; -import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.generation.type.KeyType; import org.pgpainless.key.generation.type.ecc.EllipticCurve; -import org.pgpainless.key.generation.type.xdh.XDHSpec; +import org.pgpainless.key.generation.type.xdh_legacy.XDHLegacySpec; +import org.pgpainless.util.TestAllImplementations; public class CertificationKeyMustBeAbleToCertifyTest { @@ -22,19 +22,18 @@ public class CertificationKeyMustBeAbleToCertifyTest { * would result in an invalid key. * This test therefore verifies that generating such keys fails. */ - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void testCertificationIncapableKeyTypesThrow(ImplementationFactory implementationFactory) { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void testCertificationIncapableKeyTypesThrow() { KeyType[] typesIncapableOfCreatingVerifications = new KeyType[] { KeyType.ECDH(EllipticCurve._P256), KeyType.ECDH(EllipticCurve._P384), KeyType.ECDH(EllipticCurve._P521), - KeyType.XDH(XDHSpec._X25519) + KeyType.XDH_LEGACY(XDHLegacySpec._X25519) }; for (KeyType type : typesIncapableOfCreatingVerifications) { assertThrows(IllegalArgumentException.class, () -> PGPainless - .generateKeyRing() + .buildKeyRing() .setPrimaryKey(KeySpec.getBuilder(type, KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA)) .addUserId("should@throw.ex") .build()); diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateEllipticCurveKeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateEllipticCurveKeyTest.java index aa128ec5..4cb992db 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateEllipticCurveKeyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateEllipticCurveKeyTest.java @@ -6,37 +6,39 @@ package org.pgpainless.key.generation; import static org.junit.jupiter.api.Assertions.assertEquals; -import java.io.IOException; import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.KeyFlag; import org.pgpainless.algorithm.PublicKeyAlgorithm; -import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.generation.type.KeyType; -import org.pgpainless.key.generation.type.eddsa.EdDSACurve; -import org.pgpainless.key.generation.type.xdh.XDHSpec; +import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve; +import org.pgpainless.key.generation.type.xdh_legacy.XDHLegacySpec; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.key.protection.UnlockSecretKey; import org.pgpainless.key.util.UserId; +import org.pgpainless.util.TestAllImplementations; public class GenerateEllipticCurveKeyTest { - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void generateEllipticCurveKeys(ImplementationFactory implementationFactory) throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException, IOException { - ImplementationFactory.setFactoryImplementation(implementationFactory); - PGPSecretKeyRing keyRing = PGPainless.generateKeyRing() + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void generateEllipticCurveKeys() + throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { + PGPSecretKeyRing keyRing = PGPainless.buildKeyRing() .setPrimaryKey(KeySpec.getBuilder( - KeyType.EDDSA(EdDSACurve._Ed25519), + KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA)) - .addSubkey(KeySpec.getBuilder(KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_COMMS)) + .addSubkey(KeySpec.getBuilder(KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.ENCRYPT_COMMS)) .addUserId(UserId.onlyEmail("alice@wonderland.lit").toString()) .build(); - assertEquals(PublicKeyAlgorithm.EDDSA.getAlgorithmId(), keyRing.getPublicKey().getAlgorithm()); + assertEquals(PublicKeyAlgorithm.EDDSA_LEGACY.getAlgorithmId(), keyRing.getPublicKey().getAlgorithm()); + UnlockSecretKey.unlockSecretKey(keyRing.getSecretKey(), SecretKeyRingProtector.unprotectedKeys()); } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithAdditionalUserIdTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithAdditionalUserIdTest.java index a068aa69..cf12ab57 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithAdditionalUserIdTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithAdditionalUserIdTest.java @@ -7,7 +7,6 @@ package org.pgpainless.key.generation; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; -import java.io.IOException; import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; import java.util.Date; @@ -17,28 +16,30 @@ import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.junit.JUtils; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.KeyFlag; -import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.generation.type.KeyType; import org.pgpainless.key.generation.type.rsa.RsaLength; import org.pgpainless.key.util.KeyRingUtils; import org.pgpainless.key.util.UserId; +import org.pgpainless.timeframe.TestTimeFrameProvider; import org.pgpainless.util.DateUtil; +import org.pgpainless.util.TestAllImplementations; public class GenerateKeyWithAdditionalUserIdTest { - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void test(ImplementationFactory implementationFactory) throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException, IOException { - ImplementationFactory.setFactoryImplementation(implementationFactory); - Date expiration = new Date(DateUtil.now().getTime() + 60 * 1000); - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void test() throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { + Date now = DateUtil.now(); + Date expiration = TestTimeFrameProvider.defaultExpirationForCreationDate(now); + PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() .setPrimaryKey(KeySpec.getBuilder( KeyType.RSA(RsaLength._3072), - KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA, KeyFlag.ENCRYPT_COMMS)) + KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA, KeyFlag.ENCRYPT_COMMS) + .setKeyCreationDate(now)) .addUserId(UserId.onlyEmail("primary@user.id")) .addUserId(UserId.onlyEmail("additional@user.id")) .addUserId(UserId.onlyEmail("additional2@user.id")) @@ -47,12 +48,12 @@ public class GenerateKeyWithAdditionalUserIdTest { .build(); PGPPublicKeyRing publicKeys = KeyRingUtils.publicKeyRingFrom(secretKeys); - JUtils.assertEquals(expiration.getTime(), PGPainless.inspectKeyRing(publicKeys).getPrimaryKeyExpirationDate().getTime(), 2000); + JUtils.assertDateEquals(expiration, PGPainless.inspectKeyRing(publicKeys).getPrimaryKeyExpirationDate()); Iterator userIds = publicKeys.getPublicKey().getUserIDs(); - assertEquals("primary@user.id", userIds.next()); - assertEquals("additional@user.id", userIds.next()); - assertEquals("additional2@user.id", userIds.next()); + assertEquals("", userIds.next()); + assertEquals("", userIds.next()); + assertEquals("", userIds.next()); assertEquals("trimThis@user.id", userIds.next()); assertFalse(userIds.hasNext()); } diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithCustomCreationDateTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithCustomCreationDateTest.java new file mode 100644 index 00000000..0ad564db --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithCustomCreationDateTest.java @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.generation; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.util.Calendar; +import java.util.Date; +import java.util.Iterator; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.junit.JUtils; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.KeyFlag; +import org.pgpainless.key.generation.type.KeyType; +import org.pgpainless.key.generation.type.ecc.EllipticCurve; +import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve; +import org.pgpainless.key.generation.type.xdh_legacy.XDHLegacySpec; +import org.pgpainless.util.DateUtil; + +public class GenerateKeyWithCustomCreationDateTest { + + @Test + public void generateKeyWithCustomCreationDateTest() + throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + Date creationDate = DateUtil.parseUTCDate("2018-06-11 14:12:09 UTC"); + PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() + .addSubkey(KeySpec.getBuilder(KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)) + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA) + .setKeyCreationDate(creationDate)) // primary key with custom creation time + .addUserId("Alice") + .build(); + + Iterator iterator = secretKeys.iterator(); + PGPPublicKey primaryKey = iterator.next().getPublicKey(); + PGPPublicKey subkey = iterator.next().getPublicKey(); + + JUtils.assertDateEquals(creationDate, primaryKey.getCreationTime()); + // subkey has no creation date override, so it was generated "just now" + JUtils.assertDateNotEquals(creationDate, subkey.getCreationTime()); + } + + @Test + public void generateSubkeyWithFutureKeyCreationDate() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + Calendar calendar = Calendar.getInstance(); + calendar.add(Calendar.YEAR, 20); + Date future = calendar.getTime(); + + PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() + .addSubkey(KeySpec.getBuilder(KeyType.ECDH(EllipticCurve._P384), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE).setKeyCreationDate(future)) + .setPrimaryKey(KeySpec.getBuilder(KeyType.ECDSA(EllipticCurve._P384), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA)) + .addUserId("Captain Future ") + .build(); + + // Subkey has future key creation date, so its binding will predate the key -> no usable encryption key left + assertFalse(PGPainless.inspectKeyRing(secretKeys) + .isUsableForEncryption()); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithoutPrimaryKeyFlagsTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithoutPrimaryKeyFlagsTest.java new file mode 100644 index 00000000..e477aeef --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithoutPrimaryKeyFlagsTest.java @@ -0,0 +1,93 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.generation; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.DocumentSignatureType; +import org.pgpainless.algorithm.KeyFlag; +import org.pgpainless.decryption_verification.ConsumerOptions; +import org.pgpainless.decryption_verification.DecryptionStream; +import org.pgpainless.decryption_verification.MessageMetadata; +import org.pgpainless.encryption_signing.EncryptionOptions; +import org.pgpainless.encryption_signing.EncryptionResult; +import org.pgpainless.encryption_signing.EncryptionStream; +import org.pgpainless.encryption_signing.ProducerOptions; +import org.pgpainless.encryption_signing.SigningOptions; +import org.pgpainless.exception.KeyException; +import org.pgpainless.key.TestKeys; +import org.pgpainless.key.generation.type.KeyType; +import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve; +import org.pgpainless.key.generation.type.xdh_legacy.XDHLegacySpec; +import org.pgpainless.key.info.KeyRingInfo; +import org.pgpainless.key.protection.SecretKeyRingProtector; + +public class GenerateKeyWithoutPrimaryKeyFlagsTest { + + @Test + public void generateKeyWithoutCertifyKeyFlag_cannotCertifyThirdParties() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing().setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519))) + .addSubkey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.SIGN_DATA)) + .addSubkey(KeySpec.getBuilder(KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.ENCRYPT_STORAGE, KeyFlag.ENCRYPT_COMMS)) + .addUserId("Alice") + .build(); + PGPPublicKeyRing cert = PGPainless.extractCertificate(secretKeys); + + KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); + assertTrue(info.getValidUserIds().contains("Alice")); + + long primaryKeyId = info.getKeyId(); + assertTrue(info.getKeyFlagsOf("Alice").isEmpty()); + assertTrue(info.getKeyFlagsOf(primaryKeyId).isEmpty()); + assertFalse(info.isUsableForThirdPartyCertification()); + + // Key without CERTIFY_OTHER flag cannot be used to certify other keys + PGPPublicKeyRing thirdPartyCert = TestKeys.getCryptiePublicKeyRing(); + assertThrows(KeyException.UnacceptableThirdPartyCertificationKeyException.class, () -> + PGPainless.certify().certificate(thirdPartyCert) + .withKey(secretKeys, SecretKeyRingProtector.unprotectedKeys())); + + // Key without CERTIFY_OTHER flags is usable for encryption and signing + ByteArrayOutputStream ciphertext = new ByteArrayOutputStream(); + EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() + .onOutputStream(ciphertext) + .withOptions(ProducerOptions.signAndEncrypt( + EncryptionOptions.get().addRecipient(cert), + SigningOptions.get().addInlineSignature(SecretKeyRingProtector.unprotectedKeys(), secretKeys, DocumentSignatureType.BINARY_DOCUMENT) + )); + encryptionStream.write("Hello, World!\n".getBytes(StandardCharsets.UTF_8)); + encryptionStream.close(); + EncryptionResult result = encryptionStream.getResult(); + assertTrue(result.isEncryptedFor(cert)); + + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(new ByteArrayInputStream(ciphertext.toByteArray())) + .withOptions(ConsumerOptions.get().addDecryptionKey(secretKeys) + .addVerificationCert(cert)); + + ByteArrayOutputStream plaintext = new ByteArrayOutputStream(); + Streams.pipeAll(decryptionStream, plaintext); + decryptionStream.close(); + + MessageMetadata metadata = decryptionStream.getMetadata(); + assertTrue(metadata.isEncryptedFor(cert)); + assertTrue(metadata.isVerifiedSignedBy(cert)); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithoutUserIdTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithoutUserIdTest.java new file mode 100644 index 00000000..e6a5c96a --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithoutUserIdTest.java @@ -0,0 +1,105 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.generation; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.util.io.Streams; +import org.junit.JUtils; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.KeyFlag; +import org.pgpainless.decryption_verification.ConsumerOptions; +import org.pgpainless.decryption_verification.DecryptionStream; +import org.pgpainless.decryption_verification.MessageMetadata; +import org.pgpainless.decryption_verification.SignatureVerification; +import org.pgpainless.encryption_signing.EncryptionOptions; +import org.pgpainless.encryption_signing.EncryptionResult; +import org.pgpainless.encryption_signing.EncryptionStream; +import org.pgpainless.encryption_signing.ProducerOptions; +import org.pgpainless.encryption_signing.SigningOptions; +import org.pgpainless.key.generation.type.KeyType; +import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve; +import org.pgpainless.key.generation.type.xdh_legacy.XDHLegacySpec; +import org.pgpainless.key.info.KeyRingInfo; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.timeframe.TestTimeFrameProvider; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.util.Date; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class GenerateKeyWithoutUserIdTest { + + @Test + public void generateKeyWithoutUserId() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + Date now = new Date(); + Date expirationDate = TestTimeFrameProvider.defaultExpirationForCreationDate(now); + PGPSecretKeyRing secretKey = PGPainless.buildKeyRing() + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER).setKeyCreationDate(now)) + .addSubkey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.SIGN_DATA).setKeyCreationDate(now)) + .addSubkey(KeySpec.getBuilder(KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE).setKeyCreationDate(now)) + .setExpirationDate(expirationDate) + .build(); + + KeyRingInfo info = PGPainless.inspectKeyRing(secretKey); + assertNull(info.getPrimaryUserId()); + assertTrue(info.getUserIds().isEmpty()); + JUtils.assertDateEquals(expirationDate, info.getPrimaryKeyExpirationDate()); + + InputStream plaintextIn = new ByteArrayInputStream("Hello, World!\n".getBytes()); + ByteArrayOutputStream ciphertextOut = new ByteArrayOutputStream(); + SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); + PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKey); + + EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() + .onOutputStream(ciphertextOut) + .withOptions(ProducerOptions.signAndEncrypt( + EncryptionOptions.get() + .addRecipient(certificate), + SigningOptions.get() + .addSignature(protector, secretKey) + )); + Streams.pipeAll(plaintextIn, encryptionStream); + encryptionStream.close(); + + EncryptionResult result = encryptionStream.getResult(); + assertTrue(result.isEncryptedFor(certificate)); + + ByteArrayInputStream ciphertextIn = new ByteArrayInputStream(ciphertextOut.toByteArray()); + ByteArrayOutputStream plaintextOut = new ByteArrayOutputStream(); + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(ciphertextIn) + .withOptions(ConsumerOptions.get() + .addDecryptionKey(secretKey) + .addVerificationCert(certificate)); + + Streams.pipeAll(decryptionStream, plaintextOut); + decryptionStream.close(); + + MessageMetadata metadata = decryptionStream.getMetadata(); + + assertTrue(metadata.isVerifiedSignedBy(certificate), + failuresToString(metadata.getRejectedInlineSignatures())); + assertTrue(metadata.isEncrypted()); + } + + private static String failuresToString(List failureList) { + StringBuilder sb = new StringBuilder(); + for (SignatureVerification.Failure failure : failureList) { + sb.append(failure.toString()).append('\n'); + } + return sb.toString(); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateWithEmptyPassphraseTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateWithEmptyPassphraseTest.java index f16c0b1b..c1375883 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateWithEmptyPassphraseTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateWithEmptyPassphraseTest.java @@ -6,22 +6,21 @@ package org.pgpainless.key.generation; import static org.junit.jupiter.api.Assertions.assertNotNull; -import java.io.IOException; import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; import org.bouncycastle.openpgp.PGPException; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.KeyFlag; -import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.generation.type.KeyType; import org.pgpainless.key.generation.type.rsa.RsaLength; +import org.pgpainless.util.TestAllImplementations; import org.pgpainless.util.Passphrase; /** - * Reproduce behavior of https://github.com/pgpainless/pgpainless/issues/16 + * Reproduce behavior of +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.generation; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.util.HashMap; +import java.util.Map; + +import org.bouncycastle.openpgp.PGPException; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.KeyFlag; +import org.pgpainless.algorithm.PublicKeyAlgorithm; +import org.pgpainless.key.generation.type.KeyType; +import org.pgpainless.key.generation.type.rsa.RsaLength; +import org.pgpainless.policy.Policy; + +public class GeneratingWeakKeyThrowsTest { + + @Test + public void refuseToGenerateWeakPrimaryKeyTest() { + // ensure we have default public key algorithm policy set + PGPainless.getPolicy().setPublicKeyAlgorithmPolicy( + Policy.PublicKeyAlgorithmPolicy.bsi2021PublicKeyAlgorithmPolicy()); + + assertThrows(IllegalArgumentException.class, () -> + PGPainless.buildKeyRing() + .setPrimaryKey(KeySpec.getBuilder(KeyType.RSA(RsaLength._1024), + KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA))); + } + + @Test + public void refuseToAddWeakSubkeyDuringGenerationTest() { + // ensure we have default public key algorithm policy set + PGPainless.getPolicy().setPublicKeyAlgorithmPolicy( + Policy.PublicKeyAlgorithmPolicy.bsi2021PublicKeyAlgorithmPolicy()); + + KeyRingBuilder kb = PGPainless.buildKeyRing() + .setPrimaryKey(KeySpec.getBuilder(KeyType.RSA(RsaLength._4096), + KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA)); + + assertThrows(IllegalArgumentException.class, () -> + kb.addSubkey(KeySpec.getBuilder(KeyType.RSA(RsaLength._1024), + KeyFlag.ENCRYPT_COMMS))); + } + + @Test + public void allowToAddWeakKeysWithWeakPolicy() + throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + // set a weak algorithm policy + Map bitStrengths = new HashMap<>(); + bitStrengths.put(PublicKeyAlgorithm.RSA_GENERAL, 512); + + PGPainless.getPolicy().setPublicKeyAlgorithmPolicy( + new Policy.PublicKeyAlgorithmPolicy(bitStrengths)); + + PGPainless.buildKeyRing() + .setPrimaryKey(KeySpec.getBuilder(KeyType.RSA(RsaLength._4096), + KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA)) + .addSubkey(KeySpec.getBuilder(KeyType.RSA(RsaLength._1024), + KeyFlag.ENCRYPT_COMMS)) + .addUserId("Henry") + .build(); + + // reset public key algorithm policy + PGPainless.getPolicy().setPublicKeyAlgorithmPolicy( + Policy.PublicKeyAlgorithmPolicy.bsi2021PublicKeyAlgorithmPolicy()); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/generation/IllegalKeyFlagsTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/generation/IllegalKeyFlagsTest.java index e5c48d5b..a00e58c7 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/generation/IllegalKeyFlagsTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/generation/IllegalKeyFlagsTest.java @@ -6,33 +6,32 @@ package org.pgpainless.key.generation; import static org.junit.jupiter.api.Assertions.assertThrows; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; import org.pgpainless.algorithm.KeyFlag; -import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.generation.type.KeyType; -import org.pgpainless.key.generation.type.eddsa.EdDSACurve; -import org.pgpainless.key.generation.type.xdh.XDHSpec; +import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve; +import org.pgpainless.key.generation.type.xdh_legacy.XDHLegacySpec; +import org.pgpainless.util.TestAllImplementations; public class IllegalKeyFlagsTest { - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void testKeyCannotCarryFlagsTest(ImplementationFactory implementationFactory) { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void testKeyCannotCarryFlagsTest() { assertThrows(IllegalArgumentException.class, () -> KeySpec.getBuilder( - KeyType.XDH(XDHSpec._X25519), KeyFlag.SIGN_DATA)); + KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.SIGN_DATA)); assertThrows(IllegalArgumentException.class, () -> KeySpec.getBuilder( - KeyType.XDH(XDHSpec._X25519), KeyFlag.CERTIFY_OTHER)); + KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.CERTIFY_OTHER)); assertThrows(IllegalArgumentException.class, () -> KeySpec.getBuilder( - KeyType.XDH(XDHSpec._X25519), KeyFlag.AUTHENTICATION)); + KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.AUTHENTICATION)); assertThrows(IllegalArgumentException.class, () -> KeySpec.getBuilder( - KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.ENCRYPT_COMMS)); + KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.ENCRYPT_COMMS)); assertThrows(IllegalArgumentException.class, () -> KeySpec.getBuilder( - KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.ENCRYPT_STORAGE)); + KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.ENCRYPT_STORAGE)); } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/generation/KeyGenerationSubpacketsTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/generation/KeyGenerationSubpacketsTest.java new file mode 100644 index 00000000..238d1a40 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/key/generation/KeyGenerationSubpacketsTest.java @@ -0,0 +1,164 @@ +// SPDX-FileCopyrightText: 2021 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.generation; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; + +import org.bouncycastle.bcpg.sig.IssuerFingerprint; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.PGPSignatureSubpacketVector; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.HashAlgorithm; +import org.pgpainless.algorithm.KeyFlag; +import org.pgpainless.key.generation.type.KeyType; +import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve; +import org.pgpainless.key.generation.type.xdh_legacy.XDHLegacySpec; +import org.pgpainless.key.info.KeyRingInfo; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.signature.subpackets.SelfSignatureSubpackets; +import org.pgpainless.util.Passphrase; + +public class KeyGenerationSubpacketsTest { + + @Test + public void verifyDefaultSubpacketsForUserIdSignatures() + throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().modernKeyRing("Alice"); + + KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); + PGPSignature userIdSig = info.getLatestUserIdCertification("Alice"); + assertNotNull(userIdSig); + int keyFlags = userIdSig.getHashedSubPackets().getKeyFlags(); + int[] preferredHashAlgorithms = userIdSig.getHashedSubPackets().getPreferredHashAlgorithms(); + int[] preferredSymmetricAlgorithms = userIdSig.getHashedSubPackets().getPreferredSymmetricAlgorithms(); + int[] preferredCompressionAlgorithms = userIdSig.getHashedSubPackets().getPreferredCompressionAlgorithms(); + assureSignatureHasDefaultSubpackets(userIdSig, secretKeys, KeyFlag.CERTIFY_OTHER); + assertTrue(userIdSig.getHashedSubPackets().isPrimaryUserID()); + + assertEquals("Alice", info.getPrimaryUserId()); + + secretKeys = PGPainless.modifyKeyRing(secretKeys) + .addUserId("Bob", + new SelfSignatureSubpackets.Callback() { + @Override + public void modifyHashedSubpackets(SelfSignatureSubpackets hashedSubpackets) { + hashedSubpackets.setPrimaryUserId(); + } + }, + SecretKeyRingProtector.unprotectedKeys()) + .addUserId("Alice", SecretKeyRingProtector.unprotectedKeys()) + .done(); + + info = PGPainless.inspectKeyRing(secretKeys); + + userIdSig = info.getLatestUserIdCertification("Alice"); + assertNotNull(userIdSig); + assureSignatureHasDefaultSubpackets(userIdSig, secretKeys, KeyFlag.CERTIFY_OTHER); + assertFalse(userIdSig.getHashedSubPackets().isPrimaryUserID()); + assertEquals(keyFlags, userIdSig.getHashedSubPackets().getKeyFlags()); + assertArrayEquals(preferredHashAlgorithms, userIdSig.getHashedSubPackets().getPreferredHashAlgorithms()); + assertArrayEquals(preferredSymmetricAlgorithms, userIdSig.getHashedSubPackets().getPreferredSymmetricAlgorithms()); + assertArrayEquals(preferredCompressionAlgorithms, userIdSig.getHashedSubPackets().getPreferredCompressionAlgorithms()); + + userIdSig = info.getLatestUserIdCertification("Bob"); + assertNotNull(userIdSig); + assureSignatureHasDefaultSubpackets(userIdSig, secretKeys, KeyFlag.CERTIFY_OTHER); + assertTrue(userIdSig.getHashedSubPackets().isPrimaryUserID()); + assertArrayEquals(preferredHashAlgorithms, userIdSig.getHashedSubPackets().getPreferredHashAlgorithms()); + assertArrayEquals(preferredSymmetricAlgorithms, userIdSig.getHashedSubPackets().getPreferredSymmetricAlgorithms()); + assertArrayEquals(preferredCompressionAlgorithms, userIdSig.getHashedSubPackets().getPreferredCompressionAlgorithms()); + + assertEquals("Bob", info.getPrimaryUserId()); + + Date now = new Date(); + Date t1 = new Date(now.getTime() + 1000 * 60 * 60); + secretKeys = PGPainless.modifyKeyRing(secretKeys, t1) + .addUserId("Alice", new SelfSignatureSubpackets.Callback() { + @Override + public void modifyHashedSubpackets(SelfSignatureSubpackets hashedSubpackets) { + hashedSubpackets.setPrimaryUserId(); + hashedSubpackets.setPreferredHashAlgorithms(HashAlgorithm.SHA1); + } + }, SecretKeyRingProtector.unprotectedKeys()) + .done(); + info = PGPainless.inspectKeyRing(secretKeys, t1); + assertEquals("Alice", info.getPrimaryUserId()); + assertEquals(Collections.singleton(HashAlgorithm.SHA1), info.getPreferredHashAlgorithms("Alice")); + } + + @Test + public void verifyDefaultSubpacketsForSubkeyBindingSignatures() + throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().modernKeyRing("Alice"); + KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); + List keysBefore = info.getPublicKeys(); + + secretKeys = PGPainless.modifyKeyRing(secretKeys) + .addSubKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.SIGN_DATA).build(), + Passphrase.emptyPassphrase(), SecretKeyRingProtector.unprotectedKeys()) + .done(); + + + info = PGPainless.inspectKeyRing(secretKeys); + List keysAfter = new ArrayList<>(info.getPublicKeys()); + keysAfter.removeAll(keysBefore); + assertEquals(1, keysAfter.size()); + PGPPublicKey newSigningKey = keysAfter.get(0); + + PGPSignature bindingSig = info.getCurrentSubkeyBindingSignature(newSigningKey.getKeyID()); + assertNotNull(bindingSig); + assureSignatureHasDefaultSubpackets(bindingSig, secretKeys, KeyFlag.SIGN_DATA); + assertNotNull(bindingSig.getHashedSubPackets().getEmbeddedSignatures().get(0)); + + secretKeys = PGPainless.modifyKeyRing(secretKeys) + .addSubKey(KeySpec.getBuilder(KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.ENCRYPT_COMMS).build(), + Passphrase.emptyPassphrase(), + new SelfSignatureSubpackets.Callback() { + @Override + public void modifyHashedSubpackets(SelfSignatureSubpackets hashedSubpackets) { + hashedSubpackets.setIssuerFingerprint((IssuerFingerprint) null); + } + }, SecretKeyRingProtector.unprotectedKeys()) + .done(); + + info = PGPainless.inspectKeyRing(secretKeys); + keysAfter = new ArrayList<>(info.getPublicKeys()); + keysAfter.removeAll(keysBefore); + keysAfter.remove(newSigningKey); + assertEquals(1, keysAfter.size()); + PGPPublicKey newEncryptionKey = keysAfter.get(0); + bindingSig = info.getCurrentSubkeyBindingSignature(newEncryptionKey.getKeyID()); + assertNotNull(bindingSig); + assertNull(bindingSig.getHashedSubPackets().getIssuerFingerprint()); + assertEquals(KeyFlag.toBitmask(KeyFlag.ENCRYPT_COMMS), bindingSig.getHashedSubPackets().getKeyFlags()); + } + + private void assureSignatureHasDefaultSubpackets(PGPSignature signature, PGPSecretKeyRing secretKeys, KeyFlag... keyFlags) { + PGPSignatureSubpacketVector hashedSubpackets = signature.getHashedSubPackets(); + assertNotNull(hashedSubpackets.getIssuerFingerprint()); + assertEquals(secretKeys.getPublicKey().getKeyID(), hashedSubpackets.getIssuerKeyID()); + assertArrayEquals( + secretKeys.getPublicKey().getFingerprint(), + hashedSubpackets.getIssuerFingerprint().getFingerprint()); + assertEquals(hashedSubpackets.getKeyFlags(), KeyFlag.toBitmask(keyFlags)); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/generation/StupidAlgorithmPreferenceEncryptionTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/generation/StupidAlgorithmPreferenceEncryptionTest.java new file mode 100644 index 00000000..19d5fdd1 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/key/generation/StupidAlgorithmPreferenceEncryptionTest.java @@ -0,0 +1,120 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.generation; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.KeyFlag; +import org.pgpainless.algorithm.SymmetricKeyAlgorithm; +import org.pgpainless.encryption_signing.EncryptionOptions; +import org.pgpainless.encryption_signing.EncryptionResult; +import org.pgpainless.encryption_signing.EncryptionStream; +import org.pgpainless.encryption_signing.ProducerOptions; +import org.pgpainless.key.generation.type.KeyType; +import org.pgpainless.key.generation.type.rsa.RsaLength; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class StupidAlgorithmPreferenceEncryptionTest { + + @Test + public void testPreventUnencryptedAlgorithmPreferenceDuringKeyGeneration() { + KeySpecBuilder specBuilder = KeySpec.getBuilder(KeyType.RSA(RsaLength._4096), KeyFlag.CERTIFY_OTHER); + assertThrows(IllegalArgumentException.class, () -> + specBuilder.overridePreferredSymmetricKeyAlgorithms( + SymmetricKeyAlgorithm.AES_256, SymmetricKeyAlgorithm.AES_192, + SymmetricKeyAlgorithm.AES_128, SymmetricKeyAlgorithm.NULL)); + } + + // RSA key with symmetric algorithm preference "NULL" (unencrypted). + private static final String STUPID_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 6CA6 C4F3 CF01 CAC9 C954 0BD6 5BDA 78ED C479 A8E9\n" + + "Comment: This is Stupid\n" + + "\n" + + "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\n" + + "WFv0UlYdnSNZfY0vroIMxnM1w4XJVg==\n" + + "=4plS\n" + + "-----END PGP PRIVATE KEY BLOCK-----\n"; + + @Test + public void testEncryptionIsNotUnencrypted() throws PGPException, IOException { + PGPSecretKeyRing stupidKey = PGPainless.readKeyRing().secretKeyRing(STUPID_KEY); + PGPPublicKeyRing certificate = PGPainless.extractCertificate(stupidKey); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() + .onOutputStream(out) + .withOptions(ProducerOptions.encrypt( + new EncryptionOptions().addRecipient(certificate) + )); + + encryptionStream.write("Hello".getBytes(StandardCharsets.UTF_8)); + encryptionStream.close(); + + EncryptionResult metadata = encryptionStream.getResult(); + assertTrue(metadata.isEncryptedFor(certificate)); + assertEquals(PGPainless.getPolicy().getSymmetricKeyEncryptionAlgorithmPolicy().getDefaultSymmetricKeyAlgorithm(), + metadata.getEncryptionAlgorithm()); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/info/KeyRingInfoTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/info/KeyRingInfoTest.java index 4a344f98..34465bba 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/info/KeyRingInfoTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/info/KeyRingInfoTest.java @@ -31,8 +31,8 @@ import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.junit.JUtils; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.CompressionAlgorithm; import org.pgpainless.algorithm.EncryptionPurpose; @@ -40,26 +40,26 @@ import org.pgpainless.algorithm.HashAlgorithm; import org.pgpainless.algorithm.KeyFlag; import org.pgpainless.algorithm.PublicKeyAlgorithm; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; -import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.OpenPgpV4Fingerprint; import org.pgpainless.key.TestKeys; import org.pgpainless.key.generation.KeySpec; import org.pgpainless.key.generation.type.KeyType; import org.pgpainless.key.generation.type.ecc.EllipticCurve; -import org.pgpainless.key.generation.type.eddsa.EdDSACurve; +import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnprotectedKeysProtector; import org.pgpainless.key.util.KeyRingUtils; +import org.pgpainless.key.util.RevocationAttributes; import org.pgpainless.key.util.UserId; import org.pgpainless.util.DateUtil; import org.pgpainless.util.Passphrase; +import org.pgpainless.util.TestAllImplementations; public class KeyRingInfoTest { - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void testWithEmilsKeys(ImplementationFactory implementationFactory) throws IOException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void testWithEmilsKeys() throws IOException, PGPException { PGPSecretKeyRing secretKeys = TestKeys.getEmilSecretKeyRing(); PGPPublicKeyRing publicKeys = TestKeys.getEmilPublicKeyRing(); @@ -106,7 +106,12 @@ public class KeyRingInfoTest { assertNull(sInfo.getRevocationDate()); assertNull(pInfo.getRevocationDate()); Date revocationDate = DateUtil.now(); - PGPSecretKeyRing revoked = PGPainless.modifyKeyRing(secretKeys).revoke(new UnprotectedKeysProtector()).done(); + PGPSecretKeyRing revoked = PGPainless.modifyKeyRing(secretKeys).revoke( + new UnprotectedKeysProtector(), + RevocationAttributes.createKeyRevocation() + .withReason(RevocationAttributes.Reason.KEY_RETIRED) + .withoutDescription() + ).done(); KeyRingInfo rInfo = PGPainless.inspectKeyRing(revoked); assertNotNull(rInfo.getRevocationDate()); assertEquals(revocationDate.getTime(), rInfo.getRevocationDate().getTime(), 5); @@ -144,7 +149,7 @@ public class KeyRingInfoTest { private static PGPSecretKeyRing encryptSecretKeys(PGPSecretKeyRing secretKeys) throws PGPException { return PGPainless.modifyKeyRing(secretKeys) - .changePassphraseFromOldPassphrase(null) + .changePassphraseFromOldPassphrase(Passphrase.emptyPassphrase()) .withSecureDefaultSettings() .toNewPassphrase(Passphrase.fromPassword("sw0rdf1sh")) .done(); @@ -174,10 +179,9 @@ public class KeyRingInfoTest { KeyRingUtils.requireSecretKeyFrom(secretKeys, secretKeys.getPublicKey().getKeyID())); } - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void dummyS2KTest(ImplementationFactory implementationFactory) throws PGPException, IOException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void dummyS2KTest() throws IOException { String withDummyS2K = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + "\n" + @@ -213,14 +217,14 @@ public class KeyRingInfoTest { assertTrue(new KeyInfo(secretKeys.getSecretKey()).hasDummyS2K()); } - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void testGetKeysWithFlagsAndExpiry(ImplementationFactory implementationFactory) throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void testGetKeysWithFlagsAndExpiry() + throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() + PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() .setPrimaryKey(KeySpec.getBuilder( - KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER)) + KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER)) .addSubkey(KeySpec.getBuilder( KeyType.ECDH(EllipticCurve._BRAINPOOLP384R1), KeyFlag.ENCRYPT_STORAGE)) @@ -240,19 +244,15 @@ public class KeyRingInfoTest { calendar.setTime(now); calendar.add(Calendar.DATE, 10); - Date encryptionKeyExpiration = calendar.getTime(); // in 10 days PGPSecretKey encryptionKey = keys.next(); calendar.setTime(now); calendar.add(Calendar.DATE, 3); - Date signingKeyExpiration = calendar.getTime(); // in 3 days PGPSecretKey signingKey = keys.next(); SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); secretKeys = PGPainless.modifyKeyRing(secretKeys) - .setExpirationDate(new OpenPgpV4Fingerprint(primaryKey), primaryKeyExpiration, protector) - .setExpirationDate(new OpenPgpV4Fingerprint(encryptionKey), encryptionKeyExpiration, protector) - .setExpirationDate(new OpenPgpV4Fingerprint(signingKey), signingKeyExpiration, protector) + .setExpirationDate(primaryKeyExpiration, protector) .done(); KeyRingInfo info = new KeyRingInfo(secretKeys); @@ -269,11 +269,13 @@ public class KeyRingInfoTest { assertEquals(1, certKeys.size()); assertEquals(primaryKey.getKeyID(), certKeys.get(0).getKeyID()); + assertNotNull(info.getPrimaryKeyExpirationDate()); assertEquals(primaryKeyExpiration.getTime(), info.getPrimaryKeyExpirationDate().getTime(), 5); - assertEquals(signingKeyExpiration.getTime(), info.getExpirationDateForUse(KeyFlag.SIGN_DATA).getTime(), 5); // Encryption key expires after primary key, so we return primary key expiration instead. - assertEquals(primaryKeyExpiration.getTime(), info.getExpirationDateForUse(KeyFlag.ENCRYPT_STORAGE).getTime(), 5); + Date encryptExpDate = info.getExpirationDateForUse(KeyFlag.ENCRYPT_STORAGE); + assertNotNull(encryptExpDate); + assertEquals(primaryKeyExpiration.getTime(), encryptExpDate.getTime(), 5); } @@ -522,7 +524,7 @@ public class KeyRingInfoTest { @Test public void getSecretKeyTest() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().modernKeyRing("Alice", null); + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().modernKeyRing("Alice"); KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); OpenPgpV4Fingerprint primaryKeyFingerprint = new OpenPgpV4Fingerprint(secretKeys); @@ -556,9 +558,9 @@ public class KeyRingInfoTest { @Test public void testGetExpirationDateForUse_NoSuchKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() + PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() .addUserId("Alice") - .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER)) + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER)) .build(); KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); @@ -605,40 +607,34 @@ public class KeyRingInfoTest { KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); // Bob is an invalid userId - assertThrows(IllegalArgumentException.class, () -> info.getPreferredSymmetricKeyAlgorithms("Bob", 0)); - assertThrows(IllegalArgumentException.class, () -> info.getPreferredSymmetricKeyAlgorithms("Bob", pkid)); + assertThrows(NoSuchElementException.class, () -> info.getPreferredSymmetricKeyAlgorithms("Bob")); // 123 is an invalid keyid - assertThrows(IllegalArgumentException.class, () -> info.getPreferredSymmetricKeyAlgorithms(null, 123L)); - assertThrows(IllegalArgumentException.class, () -> info.getPreferredSymmetricKeyAlgorithms("Alice", 123L)); + assertThrows(NoSuchElementException.class, () -> info.getPreferredSymmetricKeyAlgorithms(123L)); - assertEquals(preferredHashAlgorithms, info.getPreferredHashAlgorithms("Alice", pkid)); - assertEquals(preferredHashAlgorithms, info.getPreferredHashAlgorithms(null, pkid)); - assertEquals(preferredHashAlgorithms, info.getPreferredHashAlgorithms(null, skid1)); - assertEquals(preferredHashAlgorithms, info.getPreferredHashAlgorithms(null, skid2)); + assertEquals(preferredHashAlgorithms, info.getPreferredHashAlgorithms("Alice")); + assertEquals(preferredHashAlgorithms, info.getPreferredHashAlgorithms(pkid)); + assertEquals(preferredHashAlgorithms, info.getPreferredHashAlgorithms(skid1)); + assertEquals(preferredHashAlgorithms, info.getPreferredHashAlgorithms(skid2)); // Bob is an invalid userId - assertThrows(IllegalArgumentException.class, () -> info.getPreferredCompressionAlgorithms("Bob", 0)); - assertThrows(IllegalArgumentException.class, () -> info.getPreferredCompressionAlgorithms("Bob", pkid)); + assertThrows(NoSuchElementException.class, () -> info.getPreferredCompressionAlgorithms("Bob")); // 123 is an invalid keyid - assertThrows(IllegalArgumentException.class, () -> info.getPreferredCompressionAlgorithms(null, 123L)); - assertThrows(IllegalArgumentException.class, () -> info.getPreferredCompressionAlgorithms("Alice", 123L)); + assertThrows(NoSuchElementException.class, () -> info.getPreferredCompressionAlgorithms(123L)); - assertEquals(preferredCompressionAlgorithms, info.getPreferredCompressionAlgorithms("Alice", pkid)); - assertEquals(preferredCompressionAlgorithms, info.getPreferredCompressionAlgorithms(null, pkid)); - assertEquals(preferredCompressionAlgorithms, info.getPreferredCompressionAlgorithms(null, skid1)); - assertEquals(preferredCompressionAlgorithms, info.getPreferredCompressionAlgorithms(null, skid2)); + assertEquals(preferredCompressionAlgorithms, info.getPreferredCompressionAlgorithms("Alice")); + assertEquals(preferredCompressionAlgorithms, info.getPreferredCompressionAlgorithms(pkid)); + assertEquals(preferredCompressionAlgorithms, info.getPreferredCompressionAlgorithms(skid1)); + assertEquals(preferredCompressionAlgorithms, info.getPreferredCompressionAlgorithms(skid2)); // Bob is an invalid userId - assertThrows(IllegalArgumentException.class, () -> info.getPreferredSymmetricKeyAlgorithms("Bob", 0)); - assertThrows(IllegalArgumentException.class, () -> info.getPreferredSymmetricKeyAlgorithms("Bob", pkid)); + assertThrows(NoSuchElementException.class, () -> info.getPreferredSymmetricKeyAlgorithms("Bob")); // 123 is an invalid keyid - assertThrows(IllegalArgumentException.class, () -> info.getPreferredSymmetricKeyAlgorithms(null, 123L)); - assertThrows(IllegalArgumentException.class, () -> info.getPreferredSymmetricKeyAlgorithms("Alice", 123L)); + assertThrows(NoSuchElementException.class, () -> info.getPreferredSymmetricKeyAlgorithms(123L)); - assertEquals(preferredSymmetricAlgorithms, info.getPreferredSymmetricKeyAlgorithms("Alice", pkid)); - assertEquals(preferredSymmetricAlgorithms, info.getPreferredSymmetricKeyAlgorithms(null, pkid)); - assertEquals(preferredSymmetricAlgorithms, info.getPreferredSymmetricKeyAlgorithms(null, skid1)); - assertEquals(preferredSymmetricAlgorithms, info.getPreferredSymmetricKeyAlgorithms(null, skid2)); + assertEquals(preferredSymmetricAlgorithms, info.getPreferredSymmetricKeyAlgorithms("Alice")); + assertEquals(preferredSymmetricAlgorithms, info.getPreferredSymmetricKeyAlgorithms(pkid)); + assertEquals(preferredSymmetricAlgorithms, info.getPreferredSymmetricKeyAlgorithms(skid1)); + assertEquals(preferredSymmetricAlgorithms, info.getPreferredSymmetricKeyAlgorithms(skid2)); } @@ -698,7 +694,7 @@ public class KeyRingInfoTest { assertFalse(info.isKeyValidlyBound(unboundKey.getKeyId())); - List encryptionSubkeys = info.getEncryptionSubkeys(EncryptionPurpose.STORAGE_AND_COMMUNICATIONS); + List encryptionSubkeys = info.getEncryptionSubkeys(EncryptionPurpose.ANY); assertTrue(encryptionSubkeys.stream().map(OpenPgpV4Fingerprint::new).noneMatch(f -> f.equals(unboundKey)), "Unbound subkey MUST NOT be considered a valid encryption subkey"); @@ -714,4 +710,161 @@ public class KeyRingInfoTest { assertTrue(unboundKeyCreation.after(latestModification)); assertTrue(unboundKeyCreation.after(latestKeyCreation)); } + + @Test + public void getEmailsTest() throws IOException { + // NOTE: The User-ID Format for the ID "Alice Anderson [Primary Mail Address]" is incorrect. + // TODO: Fix? + String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: B4A8 9FE8 9D59 31E6 BCF7 DC2F 6BA1 2CC7 9A08 8D73\n" + + "Comment: Alice Anderson [Primary Mail Address]\n" + + "Comment: Alice A. \n" + + "Comment: \n" + + "Comment: alice@rfc4880.spec\n" + + "Comment: alice anderson@invalid.mail\n" + + "Comment: Alice Anderson \n" + + "\n" + + "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\n" + + "=ZRAy\n" + + "-----END PGP PRIVATE KEY BLOCK-----\n"; + + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); + KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); + + List emails = info.getEmailAddresses(); + assertEquals(emails, Arrays.asList("alice@email.tld", "alice@pgpainless.org", "alice@openpgp.org", "alice@rfc4880.spec")); + } + + @Test + public void isUsableForEncryptionTest_base() throws IOException { + String CERT = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 9B6A C43E A67C 11BB C023 4CC3 69D5 9A7C 29C0 F858\n" + + "Comment: Usable \n" + + "\n" + + "mDMEYiS54BYJKwYBBAHaRw8BAQdAr0FXsDQtIpF54UwfjQb+8XJ3jxt3LkpCh0e7\n" + + "lH59Vzy0HlVzYWJsZSA8dXNhYmxlQHBncGFpbmxlc3Mub3JnPoiPBBMWCgBBBQJi\n" + + "JLngCRBp1Zp8KcD4WBYhBJtqxD6mfBG7wCNMw2nVmnwpwPhYAp4BApsBBRYCAwEA\n" + + "BAsJCAcFFQoJCAsCmQEAACuNAQDX+7/ffM2B9qaW+F9MkeUJeq9u8MLk+BcaotQZ\n" + + "/c+8pQD/RhaVmKTLjm+RmpG2O1lrkta4L5CQQBXYdNMnebhlLAu4OARiJLngEgor\n" + + "BgEEAZdVAQUBAQdA8Et257jQXR0oJOimAWU9Z5Erq5OcfguBI28ixgw5z2IDAQgH\n" + + "iHUEGBYKAB0FAmIkueACngECmwwFFgIDAQAECwkIBwUVCgkICwAKCRBp1Zp8KcD4\n" + + "WDQYAQDtJG06gAiFk7D1EqdtoTgBeIXi6pdKJ8VQA17/Sel1PgEAjO7Gy+RishFG\n" + + "eT0WwimGAGWOFgyIB8GCmuk1sEN+9wO4MwRiJLngFgkrBgEEAdpHDwEBB0BNGWZx\n" + + "IiCzs6Acu/e7Di9E+uUZmEA7geObWgwPleedLYjVBBgWCgB9BQJiJLngAp4BApsC\n" + + "BRYCAwEABAsJCAcFFQoJCAtfIAQZFgoABgUCYiS54AAKCRBsyz3UPPzzw6bTAQCZ\n" + + "4NnXfhuyw2itPKNnVSvPl72GgHzfVb2MZi2QBPFJyQD+K7Xl6qNcaI9VyMos8zSy\n" + + "VT74iE7Sraqu2Fck27y1wgMACgkQadWafCnA+FjLFwEAxb/GFdAoUgmY6DGIbatO\n" + + "LOIorswrgSQVZ8B1yLh1gxcA/2K3XO1Tl68O961SW60CijoBY/16EFC+mkQIzxTT\n" + + "J5wP\n" + + "=nFoO\n" + + "-----END PGP PUBLIC KEY BLOCK-----"; + + PGPPublicKeyRing cert = PGPainless.readKeyRing().publicKeyRing(CERT); + KeyRingInfo info = PGPainless.inspectKeyRing(cert); + assertTrue(info.isUsableForEncryption()); + } + + @Test + public void isUsableForEncryptionTest_commsOnly() throws IOException { + String CERT = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: B2EE 493D 1DAC 943A 1CBD B151 5F15 42D1 ACB7 D26F\n" + + "Comment: Comms Only \n" + + "\n" + + "mG8EYiS7mhMFK4EEACIDAwTENCF226L9l1i24ZpHuTK9P9kEc7neMZ1cQbJFSX9p\n" + + "ZP89dp4dnjZcAop5jzdvqjU98BgX9STZB6q2qYEG46luZoanDA0dpwzm0TENAvcr\n" + + "KoeIMqjv6dkKs5k11qtFx/K0JkNvbW1zIE9ubHkgPGNvbW1zLW9ubHlAcGdwYWlu\n" + + "bGVzcy5vcmc+iK8EExMKAEEFAmIku5sJEF8VQtGst9JvFiEEsu5JPR2slDocvbFR\n" + + "XxVC0ay30m8CngECmwMFFgIDAQAECwkIBwUVCgkICwKZAQAA3u4BgOl888SnxXys\n" + + "Ft/sPRh/hT8n0ObrxDHUgaAR5J7Sc3097u1r3ecCYaY045FYKKb23QGAjGSEEFG1\n" + + "TLbM1JMsE5H7xjjjJ5tTM6l45vkkrk3uMhsCL+QLv9pp251ctTF/JSCvuHMEYiS7\n" + + "mxIFK4EEACIDAwToE6c42GWSI0zmalisYewWvV/2Sfdo9KKgxfzX3rfldrOWFkN1\n" + + "fkLy6b01AUt3RqfwEBIJK6OrSXOlmdCiRV1Oqf20f2MGsDNXAttDApSSDJIHwV24\n" + + "3i6qylin0ujQ9KIDAQgHiJUEGBMKAB0FAmIku5sCngECmwQFFgIDAQAECwkIBwUV\n" + + "CgkICwAKCRBfFULRrLfSbwoYAYCzcZ29xIRUEHzZvAXWeHselBLdLGztZSBZKd9T\n" + + "m045mewePa780jk5o2z5Nt4Bj0EBfRxoiWt/czpy0nWpyfEeTHOx32jHHoTStjIF\n" + + "2XO/hpB2T8VXFfFKwj7U9LGkX+ciLg==\n" + + "=etPP\n" + + "-----END PGP PUBLIC KEY BLOCK-----"; + + PGPPublicKeyRing publicKeys = PGPainless.readKeyRing().publicKeyRing(CERT); + KeyRingInfo info = PGPainless.inspectKeyRing(publicKeys); + + assertTrue(info.isUsableForEncryption(EncryptionPurpose.COMMUNICATIONS)); + assertTrue(info.isUsableForEncryption(EncryptionPurpose.ANY)); + + assertFalse(info.isUsableForEncryption(EncryptionPurpose.STORAGE)); + } + + @Test + public void isUsableForEncryptionTest_encryptionKeyRevoked() throws IOException { + // encryption subkey is revoked + String CERT = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: CE65 608D 8639 E20C 61BF 077B F010 3226 1C64 5EA7\n" + + "Comment: Revoked \n" + + "\n" + + "mDMEYiS8+hYJKwYBBAHaRw8BAQdATvSKAaY5yvyOdJtZXBEXbyiWSsExOwnP2L35\n" + + "AyMPe7u0IFJldm9rZWQgPHJldm9rZWRAcGdwYWlubGVzcy5vcmc+iI8EExYKAEEF\n" + + "AmIkvPoJEPAQMiYcZF6nFiEEzmVgjYY54gxhvwd78BAyJhxkXqcCngECmwEFFgID\n" + + "AQAECwkIBwUVCgkICwKZAQAAYFQA/02fMgRnneYK17Vsxc8DJEj0pVmTDHIOQH8K\n" + + "O8BuTkvhAP9zXtnJ7BsWO3Kg/ajIlaZEzMl6/lK2FTnAzBhs1UtrD7g4BGIkvPoS\n" + + "CisGAQQBl1UBBQEBB0AO8Bzm66ydlFhKtesh9EX66k4yyODeO0X3y3JUbrAnFQMB\n" + + "CAeIdQQYFgoAHQUCYiS8+gKeAQKbDAUWAgMBAAQLCQgHBRUKCQgLAAoJEPAQMiYc\n" + + "ZF6nTB0BAPjF6pUUrS3wv8CvrIM3S4BCtCOp+oQyPsie72As+47SAP41KfnvzYF3\n" + + "Y0WBp94Dqiy1MkvMZ9Q2x8BQt/L1UsoTBIh7BCgWCgAtBQJiJLz8CRDwEDImHGRe\n" + + "pxYhBM5lYI2GOeIMYb8He/AQMiYcZF6nAocAAh0DAAABqgD/TJpSDZ5fX3zNHqmN\n" + + "4TOuJ1GEkiYpPjBhem2C+U9jHjoBAJxQqzDB2VMiUDfe2+LLVIYa4EwhT2rT12qg\n" + + "aJ+TXWAJuDMEYiS8+hYJKwYBBAHaRw8BAQdAR0y6K6GPt4ddNyaRX16duqDFZwQi\n" + + "jeflFZ+UGLQ5GgSI1QQYFgoAfQUCYiS8+gKeAQKbAgUWAgMBAAQLCQgHBRUKCQgL\n" + + "XyAEGRYKAAYFAmIkvPoACgkQCX8koK2POrbPywEA3mbeGX8vWwnENtiFeMBjXNox\n" + + "oHAIuULBsvOdc1xrH0QBALezsulAJoziQ/t+EUrNHgTELDq3F8Y8tmLAJykb/nQB\n" + + "AAoJEPAQMiYcZF6n6CAA/0HadYoqOUbMjgu3Tle0HSXiTCJfBrTox5trTOKUsQ8z\n" + + "AQCjeV+3VT+u1movwIYv4XkzB6gB+B2C+DK9nvG5sXZhBg==\n" + + "=uqmO\n" + + "-----END PGP PUBLIC KEY BLOCK-----"; + PGPPublicKeyRing publicKeys = PGPainless.readKeyRing().publicKeyRing(CERT); + KeyRingInfo info = PGPainless.inspectKeyRing(publicKeys); + + assertFalse(info.isUsableForEncryption()); + assertFalse(info.isUsableForEncryption(EncryptionPurpose.ANY)); + assertFalse(info.isUsableForEncryption(EncryptionPurpose.COMMUNICATIONS)); + assertFalse(info.isUsableForEncryption(EncryptionPurpose.STORAGE)); + } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/info/UserIdRevocationTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/info/UserIdRevocationTest.java index 33768c7f..abb067d1 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/info/UserIdRevocationTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/info/UserIdRevocationTest.java @@ -28,8 +28,8 @@ import org.pgpainless.algorithm.KeyFlag; import org.pgpainless.key.TestKeys; import org.pgpainless.key.generation.KeySpec; import org.pgpainless.key.generation.type.KeyType; -import org.pgpainless.key.generation.type.eddsa.EdDSACurve; -import org.pgpainless.key.generation.type.xdh.XDHSpec; +import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve; +import org.pgpainless.key.generation.type.xdh_legacy.XDHLegacySpec; import org.pgpainless.key.protection.PasswordBasedSecretKeyRingProtector; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnprotectedKeysProtector; @@ -39,12 +39,12 @@ public class UserIdRevocationTest { @Test public void testRevocationWithoutRevocationAttributes() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() + PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() .setPrimaryKey(KeySpec.getBuilder( - KeyType.EDDSA(EdDSACurve._Ed25519), + KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.SIGN_DATA, KeyFlag.CERTIFY_OTHER)) .addSubkey(KeySpec.getBuilder( - KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_COMMS)) + KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.ENCRYPT_COMMS)) .addUserId("primary@key.id") .addUserId("secondary@key.id") .build(); @@ -77,11 +77,11 @@ public class UserIdRevocationTest { @Test public void testRevocationWithRevocationReason() throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() + PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() .setPrimaryKey(KeySpec.getBuilder( - KeyType.EDDSA(EdDSACurve._Ed25519), + KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.SIGN_DATA, KeyFlag.CERTIFY_OTHER)) - .addSubkey(KeySpec.getBuilder(KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_COMMS)) + .addSubkey(KeySpec.getBuilder(KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.ENCRYPT_COMMS)) .addUserId("primary@key.id") .addUserId("secondary@key.id") .build(); @@ -95,6 +95,7 @@ public class UserIdRevocationTest { KeyRingInfo info = new KeyRingInfo(secretKeys); PGPSignature signature = info.getUserIdRevocation("secondary@key.id"); + assertNotNull(signature); RevocationReason reason = (RevocationReason) signature.getHashedSubPackets() .getSubpacket(SignatureSubpacketTags.REVOCATION_REASON); assertNotNull(reason); diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/modification/AddSubKeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/modification/AddSubKeyTest.java index 406d300c..afcf9c98 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/modification/AddSubKeyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/modification/AddSubKeyTest.java @@ -16,15 +16,13 @@ import java.util.Iterator; import java.util.List; import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPrivateKey; import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.KeyFlag; -import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.TestKeys; import org.pgpainless.key.generation.KeySpec; import org.pgpainless.key.generation.type.ecc.EllipticCurve; @@ -34,13 +32,14 @@ import org.pgpainless.key.protection.PasswordBasedSecretKeyRingProtector; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnlockSecretKey; import org.pgpainless.util.Passphrase; +import org.pgpainless.util.TestAllImplementations; public class AddSubKeyTest { - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void testAddSubKey(ImplementationFactory implementationFactory) throws IOException, PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void testAddSubKey() + throws IOException, PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { PGPSecretKeyRing secretKeys = TestKeys.getCryptieSecretKeyRing(); List keyIdsBefore = new ArrayList<>(); @@ -65,9 +64,9 @@ public class AddSubKeyTest { long subKeyId = keyIdsAfter.get(0); PGPSecretKey subKey = secretKeys.getSecretKey(subKeyId); - SecretKeyRingProtector protector = SecretKeyRingProtector.unlockAllKeysWith( + SecretKeyRingProtector protector = SecretKeyRingProtector.unlockEachKeyWith( Passphrase.fromPassword("subKeyPassphrase"), secretKeys); - PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(subKey, protector); + UnlockSecretKey.unlockSecretKey(subKey, protector); KeyRingInfo info = new KeyRingInfo(secretKeys); assertEquals(Collections.singletonList(KeyFlag.SIGN_DATA), info.getKeyFlagsOf(subKeyId)); diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/modification/AddSubkeyWithModifiedBindingSignatureSubpackets.java b/pgpainless-core/src/test/java/org/pgpainless/key/modification/AddSubkeyWithModifiedBindingSignatureSubpackets.java new file mode 100644 index 00000000..85cddfd6 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/key/modification/AddSubkeyWithModifiedBindingSignatureSubpackets.java @@ -0,0 +1,81 @@ +// SPDX-FileCopyrightText: 2021 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.modification; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.util.Date; +import java.util.List; + +import org.bouncycastle.bcpg.sig.NotationData; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPKeyPair; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; +import org.junit.JUtils; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.KeyFlag; +import org.pgpainless.key.OpenPgpV4Fingerprint; +import org.pgpainless.key.generation.KeyRingBuilder; +import org.pgpainless.key.generation.KeySpec; +import org.pgpainless.key.generation.type.KeyType; +import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve; +import org.pgpainless.key.info.KeyRingInfo; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.signature.subpackets.SelfSignatureSubpackets; +import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil; + +public class AddSubkeyWithModifiedBindingSignatureSubpackets { + + public static final long MILLIS_IN_SEC = 1000; + + @Test + public void bindEncryptionSubkeyAndModifyBindingSignatureHashedSubpackets() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() + .modernKeyRing("Alice "); + KeyRingInfo before = PGPainless.inspectKeyRing(secretKeys); + + PGPKeyPair secretSubkey = KeyRingBuilder.generateKeyPair( + KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.SIGN_DATA).build()); + + long secondsUntilExpiration = 1000; + secretKeys = PGPainless.modifyKeyRing(secretKeys) + .addSubKey(secretSubkey, new SelfSignatureSubpackets.Callback() { + @Override + public void modifyHashedSubpackets(SelfSignatureSubpackets hashedSubpackets) { + hashedSubpackets.setKeyExpirationTime(true, secondsUntilExpiration); + hashedSubpackets.addNotationData(false, "test@test.test", "test"); + } + }, SecretKeyRingProtector.unprotectedKeys(), protector, KeyFlag.SIGN_DATA) + .done(); + + KeyRingInfo after = PGPainless.inspectKeyRing(secretKeys); + List signingKeys = after.getSigningSubkeys(); + signingKeys.removeAll(before.getSigningSubkeys()); + assertFalse(signingKeys.isEmpty()); + + PGPPublicKey newKey = signingKeys.get(0); + Date newExpirationDate = after.getSubkeyExpirationDate(new OpenPgpV4Fingerprint(newKey)); + assertNotNull(newExpirationDate); + Date now = new Date(); + JUtils.assertEquals( + now.getTime() + MILLIS_IN_SEC * secondsUntilExpiration, + newExpirationDate.getTime(), 2 * MILLIS_IN_SEC); + assertTrue(newKey.getSignatures().hasNext()); + PGPSignature binding = newKey.getSignatures().next(); + List notations = SignatureSubpacketsUtil.getHashedNotationData(binding); + assertEquals(1, notations.size()); + assertEquals("test@test.test", notations.get(0).getNotationName()); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/modification/AddUserIdTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/modification/AddUserIdTest.java index afb761ef..7e15c998 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/modification/AddUserIdTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/modification/AddUserIdTest.java @@ -6,35 +6,42 @@ package org.pgpainless.key.modification; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import java.io.IOException; import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; +import java.util.Date; import java.util.Iterator; import java.util.NoSuchElementException; +import openpgp.DateExtensionsKt; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; import org.pgpainless.PGPainless; -import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.TestKeys; +import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.PasswordBasedSecretKeyRingProtector; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnprotectedKeysProtector; +import org.pgpainless.key.util.UserId; +import org.pgpainless.util.TestAllImplementations; import org.pgpainless.util.Passphrase; public class AddUserIdTest { - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void addUserIdToExistingKeyRing(ImplementationFactory implementationFactory) throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void addUserIdToExistingKeyRing() + throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().simpleEcKeyRing("alice@wonderland.lit", "rabb1th0le"); - Iterator userIds = secretKeys.getSecretKey().getPublicKey().getUserIDs(); + KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); + Iterator userIds = info.getValidUserIds().iterator(); assertEquals("alice@wonderland.lit", userIds.next()); assertFalse(userIds.hasNext()); @@ -43,34 +50,34 @@ public class AddUserIdTest { .addUserId("cheshirecat@wonderland.lit", protector) .done(); - userIds = secretKeys.getPublicKey().getUserIDs(); + info = PGPainless.inspectKeyRing(secretKeys); + userIds = info.getValidUserIds().iterator(); assertEquals("alice@wonderland.lit", userIds.next()); assertEquals("cheshirecat@wonderland.lit", userIds.next()); assertFalse(userIds.hasNext()); secretKeys = PGPainless.modifyKeyRing(secretKeys) - .deleteUserId("cheshirecat@wonderland.lit", protector) + .revokeUserId("cheshirecat@wonderland.lit", protector) .done(); - userIds = secretKeys.getPublicKey().getUserIDs(); + info = PGPainless.inspectKeyRing(secretKeys); + userIds = info.getValidUserIds().iterator(); assertEquals("alice@wonderland.lit", userIds.next()); assertFalse(userIds.hasNext()); } - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void deleteUserId_noSuchElementExceptionForMissingUserId(ImplementationFactory implementationFactory) throws IOException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void deleteUserId_noSuchElementExceptionForMissingUserId() throws IOException, PGPException { PGPSecretKeyRing secretKeys = TestKeys.getCryptieSecretKeyRing(); assertThrows(NoSuchElementException.class, () -> PGPainless.modifyKeyRing(secretKeys) - .deleteUserId("invalid@user.id", new UnprotectedKeysProtector())); + .revokeUserId("invalid@user.id", new UnprotectedKeysProtector())); } - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void deleteExistingAndAddNewUserIdToExistingKeyRing(ImplementationFactory implementationFactory) throws PGPException, IOException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void deleteExistingAndAddNewUserIdToExistingKeyRing() throws PGPException, IOException { final String ARMORED_PRIVATE_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\r\n\r\n" + @@ -89,18 +96,36 @@ public class AddUserIdTest { "-----END PGP PRIVATE KEY BLOCK-----\r\n"; PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(ARMORED_PRIVATE_KEY); - Iterator userIds = secretKeys.getSecretKey().getPublicKey().getUserIDs(); + KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); + Iterator userIds = info.getValidUserIds().iterator(); assertEquals("", userIds.next()); assertFalse(userIds.hasNext()); SecretKeyRingProtector protector = new UnprotectedKeysProtector(); secretKeys = PGPainless.modifyKeyRing(secretKeys) - .deleteUserId("", protector) + .revokeUserId("", protector) .addUserId("cheshirecat@wonderland.lit", protector) .done(); - userIds = secretKeys.getSecretKey().getPublicKey().getUserIDs(); + info = PGPainless.inspectKeyRing(secretKeys); + userIds = info.getValidUserIds().iterator(); assertEquals("cheshirecat@wonderland.lit", userIds.next()); assertFalse(userIds.hasNext()); } + + @Test + public void addNewPrimaryUserIdTest() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + Date now = new Date(); + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() + .modernKeyRing("Alice"); + UserId bob = UserId.newBuilder().withName("Bob").noEmail().noComment().build(); + + assertNotEquals("Bob", PGPainless.inspectKeyRing(secretKeys).getPrimaryUserId()); + + secretKeys = PGPainless.modifyKeyRing(secretKeys, DateExtensionsKt.plusSeconds(now, 1)) + .addPrimaryUserId(bob, SecretKeyRingProtector.unprotectedKeys()) + .done(); + + assertEquals("Bob", PGPainless.inspectKeyRing(secretKeys, DateExtensionsKt.plusSeconds(now, 2)).getPrimaryUserId()); + } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/modification/ChangeExpirationOnKeyWithDifferentSignatureTypesTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/modification/ChangeExpirationOnKeyWithDifferentSignatureTypesTest.java index 36c9b8bb..c029a317 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/modification/ChangeExpirationOnKeyWithDifferentSignatureTypesTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/modification/ChangeExpirationOnKeyWithDifferentSignatureTypesTest.java @@ -10,12 +10,12 @@ import java.util.Date; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.junit.JUtils; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; import org.pgpainless.PGPainless; -import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.util.DateUtil; +import org.pgpainless.util.TestAllImplementations; public class ChangeExpirationOnKeyWithDifferentSignatureTypesTest { @@ -135,21 +135,19 @@ public class ChangeExpirationOnKeyWithDifferentSignatureTypesTest { "=GIQn\n" + "-----END PGP PRIVATE KEY BLOCK-----"; - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void setExpirationDate_keyHasSigClass10(ImplementationFactory implementationFactory) + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void setExpirationDate_keyHasSigClass10() throws PGPException, IOException { - ImplementationFactory.setFactoryImplementation(implementationFactory); PGPSecretKeyRing keys = PGPainless.readKeyRing().secretKeyRing(keyWithGenericCertification); SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); executeTestForKeys(keys, protector); } - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void setExpirationDate_keyHasSigClass12(ImplementationFactory implementationFactory) + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void setExpirationDate_keyHasSigClass12() throws PGPException, IOException { - ImplementationFactory.setFactoryImplementation(implementationFactory); PGPSecretKeyRing keys = PGPainless.readKeyRing().secretKeyRing(keyWithCasualCertification); SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); executeTestForKeys(keys, protector); @@ -157,9 +155,9 @@ public class ChangeExpirationOnKeyWithDifferentSignatureTypesTest { private void executeTestForKeys(PGPSecretKeyRing keys, SecretKeyRingProtector protector) throws PGPException { - Date expirationDate = new Date(new Date().getTime() + 1000 * 60 * 60 * 24 * 14); + Date expirationDate = new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24 * 14); // round date for test stability - expirationDate = DateUtil.parseUTCDate(DateUtil.formatUTCDate(expirationDate)); + expirationDate = DateUtil.toSecondsPrecision(expirationDate); PGPSecretKeyRing modded = PGPainless.modifyKeyRing(keys) .setExpirationDate(expirationDate, protector) diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/modification/ChangeExpirationTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/modification/ChangeExpirationTest.java index 562e81a7..6c0db287 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/modification/ChangeExpirationTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/modification/ChangeExpirationTest.java @@ -7,30 +7,34 @@ package org.pgpainless.key.modification; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import java.io.IOException; +import java.util.Calendar; import java.util.Date; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; +import org.junit.JUtils; +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; import org.pgpainless.PGPainless; -import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.OpenPgpV4Fingerprint; import org.pgpainless.key.TestKeys; import org.pgpainless.key.info.KeyRingInfo; +import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnprotectedKeysProtector; import org.pgpainless.util.DateUtil; +import org.pgpainless.util.TestAllImplementations; public class ChangeExpirationTest { private final OpenPgpV4Fingerprint subKeyFingerprint = new OpenPgpV4Fingerprint("F73FDE6439ABE210B1AF4EDD273EF7A0C749807B"); - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void setExpirationDateAndThenUnsetIt_OnPrimaryKey(ImplementationFactory implementationFactory) throws PGPException, IOException, InterruptedException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void setExpirationDateAndThenUnsetIt_OnPrimaryKey() + throws PGPException, IOException { PGPSecretKeyRing secretKeys = TestKeys.getEmilSecretKeyRing(); KeyRingInfo sInfo = PGPainless.inspectKeyRing(secretKeys); @@ -38,6 +42,7 @@ public class ChangeExpirationTest { assertNull(sInfo.getPrimaryKeyExpirationDate()); assertNull(sInfo.getSubkeyExpirationDate(subKeyFingerprint)); + Date now = new Date(); Date date = DateUtil.parseUTCDate("2020-11-27 16:10:32 UTC"); secretKeys = PGPainless.modifyKeyRing(secretKeys) .setExpirationDate(date, new UnprotectedKeysProtector()).done(); @@ -47,48 +52,68 @@ public class ChangeExpirationTest { // subkey unchanged assertNull(sInfo.getSubkeyExpirationDate(subKeyFingerprint)); - // We need to wait for one second as OpenPGP signatures have coarse-grained (up to a second) - // accuracy. Creating two signatures within a short amount of time will make the second one - // "invisible" - Thread.sleep(1100); - - secretKeys = PGPainless.modifyKeyRing(secretKeys) + Date t1 = new Date(now.getTime() + 1000 * 60 * 60); + secretKeys = PGPainless.modifyKeyRing(secretKeys, t1) .setExpirationDate(null, new UnprotectedKeysProtector()).done(); - sInfo = PGPainless.inspectKeyRing(secretKeys); + sInfo = PGPainless.inspectKeyRing(secretKeys, t1); assertNull(sInfo.getPrimaryKeyExpirationDate()); assertNull(sInfo.getSubkeyExpirationDate(subKeyFingerprint)); } - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void setExpirationDateAndThenUnsetIt_OnSubkey(ImplementationFactory implementationFactory) throws PGPException, IOException, InterruptedException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void setExpirationDateAndThenUnsetIt_OnSubkey() + throws PGPException, IOException { PGPSecretKeyRing secretKeys = TestKeys.getEmilSecretKeyRing(); KeyRingInfo sInfo = PGPainless.inspectKeyRing(secretKeys); - assertNull(sInfo.getSubkeyExpirationDate(subKeyFingerprint)); assertNull(sInfo.getPrimaryKeyExpirationDate()); - Date date = DateUtil.parseUTCDate("2020-11-27 16:10:32 UTC"); - secretKeys = PGPainless.modifyKeyRing(secretKeys) - .setExpirationDate(subKeyFingerprint, date, new UnprotectedKeysProtector()).done(); - sInfo = PGPainless.inspectKeyRing(secretKeys); - assertNotNull(sInfo.getSubkeyExpirationDate(subKeyFingerprint)); - assertEquals(date.getTime(), sInfo.getSubkeyExpirationDate(subKeyFingerprint).getTime()); - assertNull(sInfo.getPrimaryKeyExpirationDate()); - - // We need to wait for one second as OpenPGP signatures have coarse-grained (up to a second) - // accuracy. Creating two signatures within a short amount of time will make the second one - // "invisible" - Thread.sleep(1100); + Date now = new Date(); + Calendar calendar = Calendar.getInstance(); + calendar.setTime(now); + calendar.add(Calendar.DATE, 5); + Date expiration = calendar.getTime(); // in 5 days secretKeys = PGPainless.modifyKeyRing(secretKeys) - .setExpirationDate(subKeyFingerprint, null, new UnprotectedKeysProtector()).done(); - + .setExpirationDate(expiration, new UnprotectedKeysProtector()).done(); sInfo = PGPainless.inspectKeyRing(secretKeys); - assertNull(sInfo.getSubkeyExpirationDate(subKeyFingerprint)); + assertNotNull(sInfo.getPrimaryKeyExpirationDate()); + JUtils.assertDateEquals(expiration, sInfo.getPrimaryKeyExpirationDate()); + + Date t1 = new Date(now.getTime() + 1000 * 60 * 60); + secretKeys = PGPainless.modifyKeyRing(secretKeys, t1) + .setExpirationDate(null, new UnprotectedKeysProtector()).done(); + + sInfo = PGPainless.inspectKeyRing(secretKeys, t1); assertNull(sInfo.getPrimaryKeyExpirationDate()); } + + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void testExtremeExpirationDates() throws PGPException, IOException { + PGPSecretKeyRing secretKeys = TestKeys.getEmilSecretKeyRing(); + SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); + + // seconds from 2021 to 2199 will overflow 32bit integers + Date farAwayExpiration = DateUtil.parseUTCDate("2199-01-01 00:00:00 UTC"); + + final PGPSecretKeyRing finalKeys = secretKeys; + assertThrows(IllegalArgumentException.class, () -> + PGPainless.modifyKeyRing(finalKeys) + .setExpirationDate(farAwayExpiration, protector) + .done()); + + Date notSoFarAwayExpiration = DateUtil.parseUTCDate("2100-01-01 00:00:00 UTC"); + + secretKeys = PGPainless.modifyKeyRing(secretKeys) + .setExpirationDate(notSoFarAwayExpiration, protector) + .done(); + + Date actualExpiration = PGPainless.inspectKeyRing(secretKeys) + .getPrimaryKeyExpirationDate(); + JUtils.assertDateEquals(notSoFarAwayExpiration, actualExpiration); + } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/modification/ChangePrimaryUserIdAndExpirationDatesTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/modification/ChangePrimaryUserIdAndExpirationDatesTest.java new file mode 100644 index 00000000..834497ac --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/key/modification/ChangePrimaryUserIdAndExpirationDatesTest.java @@ -0,0 +1,233 @@ +// SPDX-FileCopyrightText: 2020 Paul Schaub , 2021 Flowcrypt a.s. +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.modification; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.key.info.KeyRingInfo; +import org.pgpainless.key.protection.SecretKeyRingProtector; + +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.util.Date; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class ChangePrimaryUserIdAndExpirationDatesTest { + + private static final long millisInHour = 1000 * 60 * 60; + + @Test + public void generateA_primaryB_revokeA_cantSecondaryA() + throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() + .modernKeyRing("A"); + SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); + + Date now = new Date(); + KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys, now); + assertFalse(info.isHardRevoked("A")); + assertFalse(info.isHardRevoked("B")); + assertIsPrimaryUserId("A", info); + assertIsNotValid("B", info); + assertIsNotPrimaryUserId("B", info); + + // One hour later + Date oneHourLater = new Date(now.getTime() + millisInHour); + secretKeys = PGPainless.modifyKeyRing(secretKeys, oneHourLater) + .addPrimaryUserId("B", protector) + .done(); + info = PGPainless.inspectKeyRing(secretKeys, oneHourLater); + + assertIsPrimaryUserId("B", info); + assertIsNotPrimaryUserId("A", info); + + // Two hours later + Date twoHoursLater = new Date(now.getTime() + 2 * millisInHour); + + secretKeys = PGPainless.modifyKeyRing(secretKeys, twoHoursLater) + .revokeUserId("A", protector) // hard revoke A + .done(); + info = PGPainless.inspectKeyRing(secretKeys, twoHoursLater); + + assertTrue(info.isHardRevoked("A")); + assertFalse(info.isHardRevoked("B")); + assertIsPrimaryUserId("B", info); + assertIsNotValid("A", info); + + // Three hours later + Date threeHoursLater = new Date(now.getTime() + 3 * millisInHour); + + PGPSecretKeyRing finalSecretKeys = secretKeys; + assertThrows(IllegalArgumentException.class, () -> + PGPainless.modifyKeyRing(finalSecretKeys, threeHoursLater).addUserId("A", protector)); + } + + @Test + public void generateA_primaryExpire_isExpired() + throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() + .modernKeyRing("A"); + SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); + + KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); + assertIsPrimaryUserId("A", info); + + Date now = new Date(); + Date later = new Date(now.getTime() + millisInHour); + + secretKeys = PGPainless.modifyKeyRing(secretKeys, now) + .setExpirationDate(later, protector) // expire the whole key + .done(); + + Date evenLater = new Date(now.getTime() + 2 * millisInHour); + + info = PGPainless.inspectKeyRing(secretKeys, evenLater); + assertFalse(info.isUserIdValid("A")); // is expired by now + } + + @Test + public void generateA_primaryB_primaryExpire_bIsStillPrimary() + throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() + .modernKeyRing("A"); + SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); + + Date now = new Date(); + // Generate key with primary user-id A + KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); + assertIsPrimaryUserId("A", info); + + // later set primary user-id to B + Date t1 = new Date(now.getTime() + millisInHour); + secretKeys = PGPainless.modifyKeyRing(secretKeys, t1) + .addPrimaryUserId("B", protector) + .done(); + info = PGPainless.inspectKeyRing(secretKeys, t1); + assertIsPrimaryUserId("B", info); + assertIsNotPrimaryUserId("A", info); + + // Even later expire the whole key + Date t2 = new Date(now.getTime() + 2 * millisInHour); + Date expiration = new Date(now.getTime() + 10 * millisInHour); + secretKeys = PGPainless.modifyKeyRing(secretKeys, t2) + .setExpirationDate(expiration, protector) // expire the whole key in 1 hour + .done(); + + Date t3 = new Date(now.getTime() + 3 * millisInHour); + + info = PGPainless.inspectKeyRing(secretKeys, t3); + assertIsValid("A", info); + assertIsValid("B", info); + assertIsPrimaryUserId("B", info); + assertIsNotPrimaryUserId("A", info); + + info = PGPainless.inspectKeyRing(secretKeys, expiration); + assertIsPrimaryUserId("B", info); // B is still primary, even though + assertFalse(info.isUserIdValid("A")); // key is expired by now + assertFalse(info.isUserIdValid("B")); + } + + @Test + public void generateA_expire_certify() + throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().modernKeyRing("A"); + SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); + + Date now = new Date(); + Date t1 = new Date(now.getTime() + millisInHour); + secretKeys = PGPainless.modifyKeyRing(secretKeys, now) + .setExpirationDate(t1, protector) + .done(); + + Date t2 = new Date(now.getTime() + 2 * millisInHour); + Date t4 = new Date(now.getTime() + 4 * millisInHour); + secretKeys = PGPainless.modifyKeyRing(secretKeys, t2) + .setExpirationDate(t4, protector) + .done(); + + KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); + assertIsValid("A", info); + assertIsPrimaryUserId("A", info); + } + + @Test + public void generateA_expire_primaryB_expire_isPrimaryB() + throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().modernKeyRing("A"); + SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); + + Date now = new Date(); + Date t1 = new Date(now.getTime() + millisInHour); + secretKeys = PGPainless.modifyKeyRing(secretKeys, t1) + .setExpirationDate(t1, protector) + .done(); + + Date t2 = new Date(now.getTime() + 2 * millisInHour); + KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys, t2); + + assertIsPrimaryUserId("A", info); + assertIsNotValid("A", info); // A is expired + + secretKeys = PGPainless.modifyKeyRing(secretKeys, t2) + .addPrimaryUserId("B", protector) + .done(); + + Date t3 = new Date(now.getTime() + 3 * millisInHour); + info = PGPainless.inspectKeyRing(secretKeys, t3); + + assertIsPrimaryUserId("B", info); + assertIsNotValid("B", info); // A and B are still expired + assertIsNotValid("A", info); + + Date t4 = new Date(now.getTime() + 4 * millisInHour); + Date t5 = new Date(now.getTime() + 5 * millisInHour); + secretKeys = PGPainless.modifyKeyRing(secretKeys, t3) + .setExpirationDate(t5, protector) + .done(); + + info = PGPainless.inspectKeyRing(secretKeys, t4); + assertIsValid("B", info); + assertIsValid("A", info); // A got re-validated when changing exp date + assertIsPrimaryUserId("B", info); + + secretKeys = PGPainless.modifyKeyRing(secretKeys, t4) + .addUserId("A", protector) // re-certify A as non-primary user-id + .done(); + info = PGPainless.inspectKeyRing(secretKeys, t4); + + assertIsValid("B", info); + assertIsValid("A", info); + assertIsPrimaryUserId("B", info); + + } + + private static void assertIsPrimaryUserId(String userId, KeyRingInfo info) { + assertEquals(userId, info.getPrimaryUserId()); + } + + private static void assertIsNotPrimaryUserId(String userId, KeyRingInfo info) { + PGPSignature signature = info.getLatestUserIdCertification(userId); + if (signature == null) { + return; + } + + assertFalse(signature.getHashedSubPackets().isPrimaryUserID()); + } + + private static void assertIsValid(String userId, KeyRingInfo info) { + assertTrue(info.isUserIdValid(userId)); + } + + private static void assertIsNotValid(String userId, KeyRingInfo info) { + assertFalse(info.isUserIdValid(userId)); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/modification/ChangeSecretKeyRingPassphraseTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/modification/ChangeSecretKeyRingPassphraseTest.java index b17a41e7..a0ea6984 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/modification/ChangeSecretKeyRingPassphraseTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/modification/ChangeSecretKeyRingPassphraseTest.java @@ -19,12 +19,9 @@ import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; -import org.bouncycastle.openpgp.operator.PGPDigestCalculatorProvider; -import org.bouncycastle.openpgp.operator.bc.BcPBESecretKeyDecryptorBuilder; -import org.bouncycastle.openpgp.operator.bc.BcPGPDigestCalculatorProvider; import org.bouncycastle.util.io.Streams; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.DocumentSignatureType; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; @@ -35,6 +32,7 @@ import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.protection.KeyRingProtectionSettings; import org.pgpainless.key.protection.PasswordBasedSecretKeyRingProtector; import org.pgpainless.key.protection.UnlockSecretKey; +import org.pgpainless.util.TestAllImplementations; import org.pgpainless.util.Passphrase; public class ChangeSecretKeyRingPassphraseTest { @@ -44,10 +42,9 @@ public class ChangeSecretKeyRingPassphraseTest { public ChangeSecretKeyRingPassphraseTest() throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { } - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void changePassphraseOfWholeKeyRingTest(ImplementationFactory implementationFactory) throws PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void changePassphraseOfWholeKeyRingTest() throws PGPException { PGPSecretKeyRing secretKeys = PGPainless.modifyKeyRing(keyRing) .changePassphraseFromOldPassphrase(Passphrase.fromPassword("weakPassphrase")) @@ -72,10 +69,9 @@ public class ChangeSecretKeyRingPassphraseTest { "Unlocking the secret key ring with the new passphrase MUST succeed."); } - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void changePassphraseOfWholeKeyRingToEmptyPassphrase(ImplementationFactory implementationFactory) throws PGPException, IOException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void changePassphraseOfWholeKeyRingToEmptyPassphrase() throws PGPException, IOException { PGPSecretKeyRing secretKeys = PGPainless.modifyKeyRing(keyRing) .changePassphraseFromOldPassphrase(Passphrase.fromPassword("weakPassphrase")) .withSecureDefaultSettings() @@ -90,10 +86,9 @@ public class ChangeSecretKeyRingPassphraseTest { signDummyMessageWithKeysAndPassphrase(changedPassphraseKeyRing, Passphrase.emptyPassphrase()); } - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void changePassphraseOfSingleSubkeyToNewPassphrase(ImplementationFactory implementationFactory) throws PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void changePassphraseOfSingleSubkeyToNewPassphrase() throws PGPException { Iterator keys = keyRing.getSecretKeys(); PGPSecretKey primaryKey = keys.next(); @@ -127,17 +122,16 @@ public class ChangeSecretKeyRingPassphraseTest { "Unlocking the subkey with the primary key passphrase must fail."); } - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void changePassphraseOfSingleSubkeyToEmptyPassphrase(ImplementationFactory implementationFactory) throws PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void changePassphraseOfSingleSubkeyToEmptyPassphrase() throws PGPException { Iterator keys = keyRing.getSecretKeys(); PGPSecretKey primaryKey = keys.next(); PGPSecretKey subKey = keys.next(); PGPSecretKeyRing secretKeys = PGPainless.modifyKeyRing(keyRing) - .changeSubKeyPassphraseFromOldPassphrase(primaryKey.getKeyID(), Passphrase.fromPassword("weakPassphrase")) + .changeSubKeyPassphraseFromOldPassphrase(subKey.getKeyID(), Passphrase.fromPassword("weakPassphrase")) .withSecureDefaultSettings() .toNoPassphrase() .done(); @@ -146,17 +140,17 @@ public class ChangeSecretKeyRingPassphraseTest { primaryKey = keys.next(); subKey = keys.next(); - extractPrivateKey(primaryKey, Passphrase.emptyPassphrase()); - extractPrivateKey(subKey, Passphrase.fromPassword("weakPassphrase")); + extractPrivateKey(primaryKey, Passphrase.fromPassword("weakPassphrase")); + extractPrivateKey(subKey, Passphrase.emptyPassphrase()); final PGPSecretKey finalPrimaryKey = primaryKey; assertThrows(PGPException.class, - () -> extractPrivateKey(finalPrimaryKey, Passphrase.fromPassword("weakPassphrase")), + () -> extractPrivateKey(finalPrimaryKey, Passphrase.emptyPassphrase()), "Unlocking the unprotected primary key with the old passphrase must fail."); final PGPSecretKey finalSubKey = subKey; assertThrows(PGPException.class, - () -> extractPrivateKey(finalSubKey, Passphrase.emptyPassphrase()), + () -> extractPrivateKey(finalSubKey, Passphrase.fromPassword("weakPassphrase")), "Unlocking the still protected subkey with an empty passphrase must fail."); } @@ -168,14 +162,12 @@ public class ChangeSecretKeyRingPassphraseTest { * @throws PGPException if passphrase is wrong */ private void extractPrivateKey(PGPSecretKey secretKey, Passphrase passphrase) throws PGPException { - PGPDigestCalculatorProvider digestCalculatorProvider = new BcPGPDigestCalculatorProvider(); if (passphrase.isEmpty() && secretKey.getKeyEncryptionAlgorithm() != SymmetricKeyAlgorithm.NULL.getAlgorithmId()) { throw new PGPException("Cannot unlock encrypted private key with empty passphrase."); } else if (!passphrase.isEmpty() && secretKey.getKeyEncryptionAlgorithm() == SymmetricKeyAlgorithm.NULL.getAlgorithmId()) { throw new PGPException("Cannot unlock unprotected private key with non-empty passphrase."); } - PBESecretKeyDecryptor decryptor = passphrase.isEmpty() ? null : new BcPBESecretKeyDecryptorBuilder(digestCalculatorProvider) - .build(passphrase.getChars()); + PBESecretKeyDecryptor decryptor = passphrase.isEmpty() ? null : ImplementationFactory.getInstance().getPBESecretKeyDecryptor(passphrase); UnlockSecretKey.unlockSecretKey(secretKey, decryptor); } diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/modification/ChangeSubkeyExpirationTimeTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/modification/ChangeSubkeyExpirationTimeTest.java new file mode 100644 index 00000000..e1926b67 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/key/modification/ChangeSubkeyExpirationTimeTest.java @@ -0,0 +1,90 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.modification; + +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.junit.JUtils; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.EncryptionPurpose; +import org.pgpainless.key.OpenPgpFingerprint; +import org.pgpainless.key.OpenPgpV4Fingerprint; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.util.DateUtil; + +import java.io.IOException; +import java.util.Date; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class ChangeSubkeyExpirationTimeTest { + + @Test + public void changeExpirationTimeOfSubkey() { + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().modernKeyRing("Alice"); + Date now = secretKeys.getPublicKey().getCreationTime(); + Date inAnHour = new Date(now.getTime() + 1000 * 60 * 60); + PGPPublicKey encryptionKey = PGPainless.inspectKeyRing(secretKeys) + .getEncryptionSubkeys(EncryptionPurpose.ANY).get(0); + secretKeys = PGPainless.modifyKeyRing(secretKeys) + .setExpirationDateOfSubkey( + inAnHour, + encryptionKey.getKeyID(), + SecretKeyRingProtector.unprotectedKeys()) + .done(); + + JUtils.assertDateEquals(inAnHour, PGPainless.inspectKeyRing(secretKeys) + .getSubkeyExpirationDate(OpenPgpFingerprint.of(encryptionKey))); + } + + @Test + public void changeExpirationTimeOfExpiredSubkey() throws IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing( + "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: CA52 4D5D E3D8 9CD9 105B BA45 3761 076B C6B5 3000\n" + + "Comment: Alice \n" + + "\n" + + "lFgEZXHykRYJKwYBBAHaRw8BAQdATArrVxPEpuA/wcayAxRl/v1tIYJSe4MCA/fO\n" + + "84CFgpcAAP9uZkLjoBIQAjUTEiS8Wk3sui3u4mJ4WVQEpNhQSpq37g8gtBxBbGlj\n" + + "ZSA8YWxpY2VAcGdwYWlubGVzcy5vcmc+iJUEExYKAEcFAmVx8pIJEDdhB2vGtTAA\n" + + "FiEEylJNXePYnNkQW7pFN2EHa8a1MAACngECmwEFFgIDAQAECwkIBwUVCgkICwWJ\n" + + "CWYBgAKZAQAAG3oA/0iJbwyjGOTa2RlgBKdmFjlBG5uwMGheKge/aZBbdUd8AQCB\n" + + "8NFWmyLlne4hDMM2g8RFf/W156wnyTH7jTQLx2sZDJxYBGVx8pIWCSsGAQQB2kcP\n" + + "AQEHQLQt6ns7yTxLvIWXqFCekh6QEvUumhHvCTjZPXa/UxCNAAEA+FHhZ1uik6PN\n" + + "Pwli9Tp9QGddf3pwQw+OL/K7gpZO3sgQHYjVBBgWCgB9BQJlcfKSAp4BApsCBRYC\n" + + "AwEABAsJCAcFFQoJCAtfIAQZFgoABgUCZXHykgAKCRCRKlHdDPaYKjyZAQD10Km4\n" + + "Qs37yF9bntS+z9Va7AMUuBlzYF5H/nXCRuqQTAEA60q++7Xwj94yLfoAfxH0V6Wd\n" + + "L2rDJCDZ3FFMlycToQMACgkQN2EHa8a1MADmDgD9EGzH6pPYRW5vWQGXNsr7PMWK\n" + + "LlBnevc0DaVWEHTu9tcA/iezQ9R+A90qcE1+HeNIJbSB89yIoJje2vePRV/JakAI\n" + + "nF0EZXHykhIKKwYBBAGXVQEFAQEHQOiLc02OQJD9qdpsyR6bJ52Cu8rUMlEJOELz\n" + + "1858OoQyAwEIBwAA/3YkHGmnVaQvUpSwlCInOvHvjLNLH9b9Lh/OxiuSoMgIEASI\n" + + "dQQYFgoAHQUCZXHykgKeAQKbDAUWAgMBAAQLCQgHBRUKCQgLAAoJEDdhB2vGtTAA\n" + + "1nkBAPAUcHxI1O+fE/QzuLANLHDeWc3Mc09KKnWoTkt/kk5VAQCIPlKQAcmmKdYE\n" + + "Tiz8woSKLQKswKr/jVMqnUiGPsU/DoiSBBgWCgBECRA3YQdrxrUwABYhBMpSTV3j\n" + + "2JzZEFu6RTdhB2vGtTAABYJlcfL6Ap4BApsMBRYCAwEABAsJCAcFFQoJCAsFiQAA\n" + + "AGgAAMNmAQDN/TML2zdgBNkfh7TIqbI4Flx54Yi7qEjSXg0Z+tszHgD/e1Bf+xEs\n" + + "BC9ewVsyQsnj3B0FliGYaPiQeoY/FGBmYQs=\n" + + "=5Ur6\n" + + "-----END PGP PRIVATE KEY BLOCK-----" + ); + assertNotNull(secretKeys); + + // subkey is expired at 2023-12-07 16:29:46 UTC + OpenPgpFingerprint encryptionSubkey = new OpenPgpV4Fingerprint("2E541354A23C9943375EC27A3EF133ED8720D636"); + JUtils.assertDateEquals( + DateUtil.parseUTCDate("2023-12-07 16:29:46 UTC"), + PGPainless.inspectKeyRing(secretKeys).getSubkeyExpirationDate(encryptionSubkey)); + + // re-validate the subkey by setting its expiry to null (no expiry) + secretKeys = PGPainless.modifyKeyRing(secretKeys) + .setExpirationDateOfSubkey(null, encryptionSubkey.getKeyId(), SecretKeyRingProtector.unprotectedKeys()) + .done(); + + assertNull(PGPainless.inspectKeyRing(secretKeys).getSubkeyExpirationDate(encryptionSubkey)); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/modification/FixUserIdDoesNotBreakEncryptionCapabilityTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/modification/FixUserIdDoesNotBreakEncryptionCapabilityTest.java new file mode 100644 index 00000000..d3fe1b2e --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/key/modification/FixUserIdDoesNotBreakEncryptionCapabilityTest.java @@ -0,0 +1,166 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.modification; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.NoSuchElementException; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.decryption_verification.ConsumerOptions; +import org.pgpainless.decryption_verification.DecryptionStream; +import org.pgpainless.decryption_verification.MessageMetadata; +import org.pgpainless.encryption_signing.EncryptionOptions; +import org.pgpainless.encryption_signing.EncryptionResult; +import org.pgpainless.encryption_signing.EncryptionStream; +import org.pgpainless.encryption_signing.ProducerOptions; +import org.pgpainless.key.info.KeyRingInfo; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.signature.subpackets.SelfSignatureSubpackets; + +/** + * Test for #298. + */ +public class FixUserIdDoesNotBreakEncryptionCapabilityTest { + + private static final String SECRET_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "\n" + + "lFgEYsyc4hYJKwYBBAHaRw8BAQdAjm3bQ61H2E6/xzjjHjl6G+mNl72r7fwdux9f\n" + + "CXQrCpoAAQDwY5Vblm+7Dq8NfP5gqThyv+23aMBYLr3UgJAZyAgu/RDBtCQoQilv\n" + + "YiAoSilvaG5zb24gPGJqQGV2YWx1YXRpb24udGVzdD6IkAQTFggAOBYhBI70BlHo\n" + + "XvYV3ufIc8MDl+w8xmx4BQJizJziAhsjBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheA\n" + + "AAoJEMMDl+w8xmx4ZAMBAIZsBqoClMlwymvNWIENCAZMQSy9NpBABk3jDyEjbhgs\n" + + "AP9sGI7URQNUDXiV+sIzvastNX/nOZ7fkwp6Xrx+74WxC5xdBGLMnOISCisGAQQB\n" + + "l1UBBQEBB0CGU2EGdS4mvy0apuPukStWSqEDH16AFSGEeTt2GyN1IQMBCAcAAP9J\n" + + "nrIGndqzxxIUHVsoImYIu9SFl9Z1tCSia6mADTtbsA88iHgEGBYIACAWIQSO9AZR\n" + + "6F72Fd7nyHPDA5fsPMZseAUCYsyc4gIbDAAKCRDDA5fsPMZseACnAQDIR7QwBTIs\n" + + "Hfu4XIpZTyipOy6ZOEKlY3akyb9TtOq1wAD8Da+0Insssuf0J5WPqShJ/wMX3+xk\n" + + "gqeRV2HyogQ7aAE=\n" + + "=6zZo\n" + + "-----END PGP PRIVATE KEY BLOCK-----\n"; + private static final String CERTIFICATE = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "\n" + + "mDMEYsyc4hYJKwYBBAHaRw8BAQdAjm3bQ61H2E6/xzjjHjl6G+mNl72r7fwdux9f\n" + + "CXQrCpq0JChCKW9iIChKKW9obnNvbiA8YmpAZXZhbHVhdGlvbi50ZXN0PoiQBBMW\n" + + "CAA4FiEEjvQGUehe9hXe58hzwwOX7DzGbHgFAmLMnOICGyMFCwkIBwIGFQoJCAsC\n" + + "BBYCAwECHgECF4AACgkQwwOX7DzGbHhkAwEAhmwGqgKUyXDKa81YgQ0IBkxBLL02\n" + + "kEAGTeMPISNuGCwA/2wYjtRFA1QNeJX6wjO9qy01f+c5nt+TCnpevH7vhbELuDgE\n" + + "Ysyc4hIKKwYBBAGXVQEFAQEHQIZTYQZ1Lia/LRqm4+6RK1ZKoQMfXoAVIYR5O3Yb\n" + + "I3UhAwEIB4h4BBgWCAAgFiEEjvQGUehe9hXe58hzwwOX7DzGbHgFAmLMnOICGwwA\n" + + "CgkQwwOX7DzGbHgApwEAyEe0MAUyLB37uFyKWU8oqTsumThCpWN2pMm/U7TqtcAA\n" + + "/A2vtCJ7LLLn9CeVj6koSf8DF9/sZIKnkVdh8qIEO2gB\n" + + "=3sNT\n" + + "-----END PGP PUBLIC KEY BLOCK-----"; + + private static final String userIdBefore = "(B)ob (J)ohnson "; + private static final String userIdAfter = "\"(B)ob (J)ohnson\" "; + + @Test + public void manualReplaceUserIdWithFixedVersionDoesNotHinderEncryptionCapability() throws IOException, PGPException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(SECRET_KEY); + SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); + PGPSecretKeyRing modified = PGPainless.modifyKeyRing(secretKeys) + .addUserId(userIdAfter, new SelfSignatureSubpackets.Callback() { + @Override + public void modifyHashedSubpackets(SelfSignatureSubpackets hashedSubpackets) { + hashedSubpackets.setPrimaryUserId(); + } + }, protector) + .removeUserId(userIdBefore, protector) + .done(); + + KeyRingInfo before = PGPainless.inspectKeyRing(secretKeys); + KeyRingInfo after = PGPainless.inspectKeyRing(modified); + + assertTrue(before.isUsableForEncryption()); + assertTrue(before.isUsableForSigning()); + assertTrue(before.isUserIdValid(userIdBefore)); + assertFalse(before.isUserIdValid(userIdAfter)); + + assertTrue(after.isUsableForEncryption()); + assertTrue(after.isUsableForSigning()); + assertFalse(after.isUserIdValid(userIdBefore)); + assertTrue(after.isUserIdValid(userIdAfter)); + } + + @Test + public void testReplaceUserId_missingOldUserIdThrows() throws IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(SECRET_KEY); + assertThrows(NoSuchElementException.class, () -> PGPainless.modifyKeyRing(secretKeys) + .replaceUserId("missing", userIdAfter, SecretKeyRingProtector.unprotectedKeys())); + } + + @Test + public void testReplaceUserId_emptyOldUserIdThrows() throws IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(SECRET_KEY); + assertThrows(IllegalArgumentException.class, () -> PGPainless.modifyKeyRing(secretKeys) + .replaceUserId(" ", userIdAfter, SecretKeyRingProtector.unprotectedKeys())); + } + + @Test + public void testReplaceUserId_emptyNewUserIdThrows() throws IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(SECRET_KEY); + assertThrows(IllegalArgumentException.class, () -> PGPainless.modifyKeyRing(secretKeys) + .replaceUserId(userIdBefore, " ", SecretKeyRingProtector.unprotectedKeys())); + } + + @Test + public void testReplaceImplicitUserIdDoesNotBreakStuff() throws IOException, PGPException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(SECRET_KEY); + + PGPSecretKeyRing edited = PGPainless.modifyKeyRing(secretKeys) + .replaceUserId(userIdBefore, userIdAfter, SecretKeyRingProtector.unprotectedKeys()) + .done(); + + KeyRingInfo info = PGPainless.inspectKeyRing(edited); + assertTrue(info.isUserIdValid(userIdAfter)); + assertEquals(userIdAfter, info.getPrimaryUserId()); + + PGPSignature latestCertification = info.getLatestUserIdCertification(userIdAfter); + assertNotNull(latestCertification); + assertTrue(latestCertification.getHashedSubPackets().isPrimaryUserID()); + + PGPPublicKeyRing cert = PGPainless.extractCertificate(edited); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() + .onOutputStream(out) + .withOptions(ProducerOptions.encrypt(new EncryptionOptions() + .addRecipient(cert))); + + encryptionStream.write("Hello".getBytes(StandardCharsets.UTF_8)); + encryptionStream.close(); + + EncryptionResult result = encryptionStream.getResult(); + assertTrue(result.isEncryptedFor(cert)); + + ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); + ByteArrayOutputStream plain = new ByteArrayOutputStream(); + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(in) + .withOptions(new ConsumerOptions() + .addDecryptionKey(edited)); + + Streams.pipeAll(decryptionStream, plain); + decryptionStream.close(); + + MessageMetadata metadata = decryptionStream.getMetadata(); + assertTrue(metadata.isEncrypted()); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/modification/KeyRingEditorTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/modification/KeyRingEditorTest.java deleted file mode 100644 index 68774cfc..00000000 --- a/pgpainless-core/src/test/java/org/pgpainless/key/modification/KeyRingEditorTest.java +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.modification; - -import static org.junit.jupiter.api.Assertions.assertThrows; - -import org.junit.jupiter.api.Test; -import org.pgpainless.key.modification.secretkeyring.SecretKeyRingEditor; - -public class KeyRingEditorTest { - - @Test - public void testConstructorThrowsNpeForNull() { - assertThrows(NullPointerException.class, - () -> new SecretKeyRingEditor(null)); - } -} diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/modification/OldSignatureSubpacketsArePreservedOnNewSig.java b/pgpainless-core/src/test/java/org/pgpainless/key/modification/OldSignatureSubpacketsArePreservedOnNewSigTest.java similarity index 52% rename from pgpainless-core/src/test/java/org/pgpainless/key/modification/OldSignatureSubpacketsArePreservedOnNewSig.java rename to pgpainless-core/src/test/java/org/pgpainless/key/modification/OldSignatureSubpacketsArePreservedOnNewSigTest.java index a03744a3..b8c3244e 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/modification/OldSignatureSubpacketsArePreservedOnNewSig.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/modification/OldSignatureSubpacketsArePreservedOnNewSigTest.java @@ -5,10 +5,9 @@ package org.pgpainless.key.modification; import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; -import java.io.IOException; import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; import java.util.Date; @@ -17,38 +16,41 @@ import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.PGPSignatureSubpacketVector; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; import org.pgpainless.PGPainless; -import org.pgpainless.implementation.ImplementationFactory; -import org.pgpainless.key.OpenPgpV4Fingerprint; import org.pgpainless.key.protection.UnprotectedKeysProtector; +import org.pgpainless.util.TestAllImplementations; -public class OldSignatureSubpacketsArePreservedOnNewSig { +public class OldSignatureSubpacketsArePreservedOnNewSigTest { - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void verifyOldSignatureSubpacketsArePreservedOnNewExpirationDateSig(ImplementationFactory implementationFactory) - throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException, InterruptedException, IOException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + private static final long millisInHour = 1000 * 60 * 60; + + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void verifyOldSignatureSubpacketsArePreservedOnNewExpirationDateSig() + throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() .simpleEcKeyRing("Alice "); - OpenPgpV4Fingerprint subkeyFingerprint = new OpenPgpV4Fingerprint(PGPainless.inspectKeyRing(secretKeys).getPublicKeys().get(1)); - - PGPSignature oldSignature = PGPainless.inspectKeyRing(secretKeys).getCurrentSubkeyBindingSignature(subkeyFingerprint.getKeyId()); + PGPSignature oldSignature = PGPainless.inspectKeyRing(secretKeys).getLatestUserIdCertification("Alice "); + assertNotNull(oldSignature); PGPSignatureSubpacketVector oldPackets = oldSignature.getHashedSubPackets(); - assertEquals(0, oldPackets.getKeyExpirationTime()); + long oldExpiration = oldPackets.getKeyExpirationTime(); - Thread.sleep(1000); - secretKeys = PGPainless.modifyKeyRing(secretKeys) - .setExpirationDate(subkeyFingerprint, new Date(), new UnprotectedKeysProtector()) + Date now = new Date(); + Date t1 = new Date(now.getTime() + millisInHour); + Date expiration = new Date(now.getTime() + 5 * 24 * millisInHour); // in 5 days + + secretKeys = PGPainless.modifyKeyRing(secretKeys, t1) + .setExpirationDate(expiration, new UnprotectedKeysProtector()) .done(); - PGPSignature newSignature = PGPainless.inspectKeyRing(secretKeys).getCurrentSubkeyBindingSignature(subkeyFingerprint.getKeyId()); + PGPSignature newSignature = PGPainless.inspectKeyRing(secretKeys, t1).getLatestUserIdCertification("Alice "); + assertNotNull(newSignature); PGPSignatureSubpacketVector newPackets = newSignature.getHashedSubPackets(); - assertNotEquals(0, newPackets.getKeyExpirationTime()); + assertNotEquals(oldExpiration, newPackets.getKeyExpirationTime()); assertArrayEquals(oldPackets.getPreferredHashAlgorithms(), newPackets.getPreferredHashAlgorithms()); } diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/modification/RefuseToAddWeakSubkeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/modification/RefuseToAddWeakSubkeyTest.java new file mode 100644 index 00000000..04197d6f --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/key/modification/RefuseToAddWeakSubkeyTest.java @@ -0,0 +1,90 @@ +// SPDX-FileCopyrightText: 2021 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.modification; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.util.EnumMap; +import java.util.Map; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.EncryptionPurpose; +import org.pgpainless.algorithm.KeyFlag; +import org.pgpainless.algorithm.PublicKeyAlgorithm; +import org.pgpainless.key.generation.KeySpec; +import org.pgpainless.key.generation.type.KeyType; +import org.pgpainless.key.generation.type.rsa.RsaLength; +import org.pgpainless.key.modification.secretkeyring.SecretKeyRingEditorInterface; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.policy.Policy; +import org.pgpainless.util.Passphrase; + +public class RefuseToAddWeakSubkeyTest { + + @Test + public void testEditorRefusesToAddWeakSubkey() + throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + // ensure default policy is set + PGPainless.getPolicy().setPublicKeyAlgorithmPolicy(Policy.PublicKeyAlgorithmPolicy.bsi2021PublicKeyAlgorithmPolicy()); + + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() + .modernKeyRing("Alice"); + SecretKeyRingEditorInterface editor = PGPainless.modifyKeyRing(secretKeys); + KeySpec spec = KeySpec.getBuilder(KeyType.RSA(RsaLength._1024), KeyFlag.ENCRYPT_COMMS).build(); + + assertThrows(IllegalArgumentException.class, () -> + editor.addSubKey(spec, Passphrase.emptyPassphrase(), SecretKeyRingProtector.unprotectedKeys())); + } + + @Test + public void testEditorAllowsToAddWeakSubkeyIfCompliesToPublicKeyAlgorithmPolicy() + throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() + .modernKeyRing("Alice"); + + // set weak policy + Map minimalBitStrengths = new EnumMap<>(PublicKeyAlgorithm.class); + // §5.4.1 + minimalBitStrengths.put(PublicKeyAlgorithm.RSA_GENERAL, 1024); + minimalBitStrengths.put(PublicKeyAlgorithm.RSA_SIGN, 1024); + minimalBitStrengths.put(PublicKeyAlgorithm.RSA_ENCRYPT, 1024); + // Note: ElGamal is not mentioned in the BSI document. + // We assume that the requirements are similar to other DH algorithms + minimalBitStrengths.put(PublicKeyAlgorithm.ELGAMAL_ENCRYPT, 2000); + minimalBitStrengths.put(PublicKeyAlgorithm.ELGAMAL_GENERAL, 2000); + // §5.4.2 + minimalBitStrengths.put(PublicKeyAlgorithm.DSA, 2000); + // §5.4.3 + minimalBitStrengths.put(PublicKeyAlgorithm.ECDSA, 250); + // Note: EdDSA is not mentioned in the BSI document. + // We assume that the requirements are similar to other EC algorithms. + minimalBitStrengths.put(PublicKeyAlgorithm.EDDSA_LEGACY, 250); + // §7.2.1 + minimalBitStrengths.put(PublicKeyAlgorithm.DIFFIE_HELLMAN, 2000); + // §7.2.2 + minimalBitStrengths.put(PublicKeyAlgorithm.ECDH, 250); + PGPainless.getPolicy().setPublicKeyAlgorithmPolicy(new Policy.PublicKeyAlgorithmPolicy(minimalBitStrengths)); + + SecretKeyRingEditorInterface editor = PGPainless.modifyKeyRing(secretKeys); + KeySpec spec = KeySpec.getBuilder(KeyType.RSA(RsaLength._1024), KeyFlag.ENCRYPT_COMMS) + .setKeyCreationDate(editor.getReferenceTime()) // The key gets created after we instantiate the editor. + .build(); + + secretKeys = editor.addSubKey(spec, Passphrase.emptyPassphrase(), SecretKeyRingProtector.unprotectedKeys()) + .done(); + + assertEquals(2, PGPainless.inspectKeyRing(secretKeys).getEncryptionSubkeys(EncryptionPurpose.ANY).size()); + + // reset default policy + PGPainless.getPolicy().setPublicKeyAlgorithmPolicy(Policy.PublicKeyAlgorithmPolicy.bsi2021PublicKeyAlgorithmPolicy()); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/modification/RevocationCertificateTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/modification/RevocationCertificateTest.java new file mode 100644 index 00000000..48b5d5b7 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/key/modification/RevocationCertificateTest.java @@ -0,0 +1,110 @@ +// SPDX-FileCopyrightText: 2021 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.modification; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.key.TestKeys; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.key.util.KeyRingUtils; +import org.pgpainless.key.util.RevocationAttributes; +import org.pgpainless.util.CollectionUtils; + +public class RevocationCertificateTest { + + @Test + public void createRevocationCertificateTest() throws PGPException, IOException { + PGPSecretKeyRing secretKeys = TestKeys.getEmilSecretKeyRing(); + + PGPSignature revocation = PGPainless.modifyKeyRing(secretKeys) + .createRevocation(SecretKeyRingProtector.unprotectedKeys(), + RevocationAttributes.createKeyRevocation() + .withReason(RevocationAttributes.Reason.KEY_RETIRED) + .withoutDescription()); + + assertNotNull(revocation); + + assertTrue(PGPainless.inspectKeyRing(secretKeys).isKeyValidlyBound(secretKeys.getPublicKey().getKeyID())); + + // merge key and revocation certificate + PGPSecretKeyRing revokedKey = KeyRingUtils.keysPlusSecretKey( + secretKeys, + KeyRingUtils.secretKeyPlusSignature(secretKeys.getSecretKey(), revocation)); + + assertFalse(PGPainless.inspectKeyRing(revokedKey).isKeyValidlyBound(secretKeys.getPublicKey().getKeyID())); + } + + @Test + public void createMinimalRevocationCertificateTest() throws PGPException, IOException { + PGPSecretKeyRing secretKeys = TestKeys.getEmilSecretKeyRing(); + + PGPPublicKeyRing minimalRevocationCert = PGPainless.modifyKeyRing(secretKeys).createMinimalRevocationCertificate( + SecretKeyRingProtector.unprotectedKeys(), + RevocationAttributes.createKeyRevocation().withReason(RevocationAttributes.Reason.KEY_RETIRED).withoutDescription()); + + assertEquals(1, minimalRevocationCert.size()); + PGPPublicKey key = minimalRevocationCert.getPublicKey(); + assertEquals(secretKeys.getPublicKey().getKeyID(), key.getKeyID()); + assertEquals(1, CollectionUtils.iteratorToList(key.getSignatures()).size()); + assertFalse(key.getUserIDs().hasNext()); + assertFalse(key.getUserAttributes().hasNext()); + assertNull(key.getTrustData()); + + PGPPublicKeyRing originalCert = PGPainless.extractCertificate(secretKeys); + PGPPublicKeyRing mergedCert = PGPainless.mergeCertificate(originalCert, minimalRevocationCert); + + assertTrue(PGPainless.inspectKeyRing(mergedCert).getRevocationState().isSoftRevocation()); + } + + @Test + public void createMinimalRevocationCertificateForFreshKeyTest() + throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().modernKeyRing("Alice "); + + PGPPublicKeyRing minimalRevocationCert = PGPainless.modifyKeyRing(secretKeys).createMinimalRevocationCertificate( + SecretKeyRingProtector.unprotectedKeys(), + RevocationAttributes.createKeyRevocation().withReason(RevocationAttributes.Reason.KEY_RETIRED).withoutDescription()); + + assertEquals(1, minimalRevocationCert.size()); + PGPPublicKey key = minimalRevocationCert.getPublicKey(); + assertEquals(secretKeys.getPublicKey().getKeyID(), key.getKeyID()); + assertEquals(1, CollectionUtils.iteratorToList(key.getSignatures()).size()); + assertFalse(key.getUserIDs().hasNext()); + assertFalse(key.getUserAttributes().hasNext()); + assertNull(key.getTrustData()); + + PGPPublicKeyRing originalCert = PGPainless.extractCertificate(secretKeys); + PGPPublicKeyRing mergedCert = PGPainless.mergeCertificate(originalCert, minimalRevocationCert); + + assertTrue(PGPainless.inspectKeyRing(mergedCert).getRevocationState().isSoftRevocation()); + } + + @Test + public void createMinimalRevocationCertificate_wrongReason() throws PGPException, IOException { + PGPSecretKeyRing secretKeys = TestKeys.getEmilSecretKeyRing(); + assertThrows(IllegalArgumentException.class, + () -> PGPainless.modifyKeyRing(secretKeys).createMinimalRevocationCertificate( + SecretKeyRingProtector.unprotectedKeys(), + RevocationAttributes.createCertificateRevocation() + .withReason(RevocationAttributes.Reason.USER_ID_NO_LONGER_VALID) + .withoutDescription())); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/modification/RevokeKeyWithGenericCertificationSignatureTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/modification/RevokeKeyWithGenericCertificationSignatureTest.java index 794438da..b2cd85fc 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/modification/RevokeKeyWithGenericCertificationSignatureTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/modification/RevokeKeyWithGenericCertificationSignatureTest.java @@ -14,12 +14,12 @@ import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; import org.pgpainless.PGPainless; -import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.protection.UnprotectedKeysProtector; import org.pgpainless.util.ArmoredOutputStreamFactory; +import org.pgpainless.util.TestAllImplementations; /** * Test that makes sure that PGPainless can deal with keys that carry a key @@ -63,10 +63,9 @@ public class RevokeKeyWithGenericCertificationSignatureTest { } } - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void test(ImplementationFactory implementationFactory) throws IOException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void test() throws IOException, PGPException { revokeKey(SAMPLE_PRIVATE_KEY); // would crash previously } diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/modification/RevokeKeyWithoutPreferredAlgorithmsOnPrimaryKey.java b/pgpainless-core/src/test/java/org/pgpainless/key/modification/RevokeKeyWithoutPreferredAlgorithmsOnPrimaryKey.java index a218a6f9..2cbbbe87 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/modification/RevokeKeyWithoutPreferredAlgorithmsOnPrimaryKey.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/modification/RevokeKeyWithoutPreferredAlgorithmsOnPrimaryKey.java @@ -5,24 +5,20 @@ package org.pgpainless.key.modification; import java.io.IOException; -import java.util.ArrayList; import java.util.Date; -import java.util.List; import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.junit.JUtils; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; import org.pgpainless.PGPainless; -import org.pgpainless.implementation.ImplementationFactory; -import org.pgpainless.key.OpenPgpV4Fingerprint; import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.modification.secretkeyring.SecretKeyRingEditorInterface; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnprotectedKeysProtector; import org.pgpainless.util.DateUtil; +import org.pgpainless.util.TestAllImplementations; public class RevokeKeyWithoutPreferredAlgorithmsOnPrimaryKey { @@ -100,30 +96,21 @@ public class RevokeKeyWithoutPreferredAlgorithmsOnPrimaryKey { "=3Zyp\n" + "-----END PGP PRIVATE KEY BLOCK-----"; - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void testChangingExpirationTimeWithKeyWithoutPrefAlgos(ImplementationFactory implementationFactory) throws IOException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); - Date expirationDate = DateUtil.parseUTCDate(DateUtil.formatUTCDate(new Date())); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void testChangingExpirationTimeWithKeyWithoutPrefAlgos() + throws IOException, PGPException { + Date expirationDate = DateUtil.now(); PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); - List fingerprintList = new ArrayList<>(); - for (PGPSecretKey secretKey : secretKeys) { - fingerprintList.add(new OpenPgpV4Fingerprint(secretKey)); - } + SecretKeyRingProtector protector = new UnprotectedKeysProtector(); SecretKeyRingEditorInterface modify = PGPainless.modifyKeyRing(secretKeys) .setExpirationDate(expirationDate, protector); - for (int i = 1; i < fingerprintList.size(); i++) { - modify.setExpirationDate(fingerprintList.get(i), expirationDate, protector); - } secretKeys = modify.done(); KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); JUtils.assertDateEquals(expirationDate, info.getPrimaryKeyExpirationDate()); - for (OpenPgpV4Fingerprint fingerprint : fingerprintList) { - JUtils.assertDateEquals(expirationDate, info.getSubkeyExpirationDate(fingerprint)); - } } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/modification/RevokeSubKeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/modification/RevokeSubKeyTest.java index 279a94d1..ee6f0de3 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/modification/RevokeSubKeyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/modification/RevokeSubKeyTest.java @@ -4,40 +4,48 @@ package org.pgpainless.key.modification; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; import java.util.Iterator; +import org.bouncycastle.bcpg.sig.IssuerFingerprint; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSignature; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.EncryptionPurpose; import org.pgpainless.algorithm.SignatureType; -import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.OpenPgpV4Fingerprint; import org.pgpainless.key.TestKeys; import org.pgpainless.key.modification.secretkeyring.SecretKeyRingEditorInterface; import org.pgpainless.key.protection.PasswordBasedSecretKeyRingProtector; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.util.RevocationAttributes; -import org.pgpainless.util.ArmorUtils; +import org.pgpainless.signature.SignatureUtils; +import org.pgpainless.signature.subpackets.RevocationSignatureSubpackets; +import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil; +import org.pgpainless.util.TestAllImplementations; import org.pgpainless.util.Passphrase; public class RevokeSubKeyTest { - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void revokeSukeyTest(ImplementationFactory implementationFactory) throws IOException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void revokeSukeyTest() throws IOException, PGPException { PGPSecretKeyRing secretKeys = TestKeys.getCryptieSecretKeyRing(); Iterator keysIterator = secretKeys.iterator(); @@ -59,24 +67,18 @@ public class RevokeSubKeyTest { assertTrue(subKey.getPublicKey().hasRevocation()); } - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void detachedRevokeSubkeyTest(ImplementationFactory implementationFactory) throws IOException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void detachedRevokeSubkeyTest() throws IOException, PGPException { PGPSecretKeyRing secretKeys = TestKeys.getCryptieSecretKeyRing(); OpenPgpV4Fingerprint fingerprint = new OpenPgpV4Fingerprint(secretKeys); SecretKeyRingProtector protector = PasswordBasedSecretKeyRingProtector.forKey(secretKeys, Passphrase.fromPassword("password123")); PGPSignature revocationCertificate = PGPainless.modifyKeyRing(secretKeys) - .createRevocationCertificate(fingerprint, protector, RevocationAttributes.createKeyRevocation() + .createRevocation(fingerprint, protector, RevocationAttributes.createKeyRevocation() .withReason(RevocationAttributes.Reason.KEY_RETIRED) .withDescription("Key no longer used.")); - // CHECKSTYLE:OFF - System.out.println("Revocation Certificate:"); - System.out.println(ArmorUtils.toAsciiArmoredString(revocationCertificate.getEncoded())); - // CHECKSTYLE:ON - PGPPublicKey publicKey = secretKeys.getPublicKey(); assertFalse(publicKey.hasRevocation()); @@ -85,10 +87,9 @@ public class RevokeSubKeyTest { assertTrue(publicKey.hasRevocation()); } - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void testRevocationSignatureTypeCorrect(ImplementationFactory implementationFactory) throws IOException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void testRevocationSignatureTypeCorrect() throws IOException, PGPException { PGPSecretKeyRing secretKeys = TestKeys.getCryptieSecretKeyRing(); Iterator keysIterator = secretKeys.getPublicKeys(); PGPPublicKey primaryKey = keysIterator.next(); @@ -97,8 +98,8 @@ public class RevokeSubKeyTest { .forKey(secretKeys, Passphrase.fromPassword("password123")); SecretKeyRingEditorInterface editor = PGPainless.modifyKeyRing(secretKeys); - PGPSignature keyRevocation = editor.createRevocationCertificate(primaryKey.getKeyID(), protector, null); - PGPSignature subkeyRevocation = editor.createRevocationCertificate(subKey.getKeyID(), protector, null); + PGPSignature keyRevocation = editor.createRevocation(primaryKey.getKeyID(), protector, (RevocationAttributes) null); + PGPSignature subkeyRevocation = editor.createRevocation(subKey.getKeyID(), protector, (RevocationAttributes) null); assertEquals(SignatureType.KEY_REVOCATION.getCode(), keyRevocation.getSignatureType()); assertEquals(SignatureType.SUBKEY_REVOCATION.getCode(), subkeyRevocation.getSignatureType()); @@ -123,4 +124,62 @@ public class RevokeSubKeyTest { RevocationAttributes.Reason reason = RevocationAttributes.Reason.KEY_COMPROMISED; assertEquals("2 - KEY_COMPROMISED", reason.toString()); } + + @Test + public void inspectSubpacketsOnDefaultRevocationSignature() + throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().modernKeyRing("Alice"); + SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); + PGPPublicKey encryptionSubkey = PGPainless.inspectKeyRing(secretKeys) + .getEncryptionSubkeys(EncryptionPurpose.ANY).get(0); + + secretKeys = PGPainless.modifyKeyRing(secretKeys) + .revokeSubKey(encryptionSubkey.getKeyID(), protector) + .done(); + + encryptionSubkey = secretKeys.getPublicKey(encryptionSubkey.getKeyID()); + PGPSignature revocation = encryptionSubkey.getSignaturesOfType(SignatureType.SUBKEY_REVOCATION.getCode()).next(); + assertNotNull(revocation); + + assertArrayEquals( + secretKeys.getPublicKey().getFingerprint(), + revocation.getHashedSubPackets().getIssuerFingerprint().getFingerprint()); + assertEquals(secretKeys.getPublicKey().getKeyID(), + revocation.getHashedSubPackets().getIssuerKeyID()); + assertNull(SignatureSubpacketsUtil.getRevocationReason(revocation)); + assertTrue(SignatureUtils.isHardRevocation(revocation)); + } + + @Test + public void inspectSubpacketsOnModifiedRevocationSignature() + throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().modernKeyRing("Alice"); + SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); + PGPPublicKey encryptionSubkey = PGPainless.inspectKeyRing(secretKeys) + .getEncryptionSubkeys(EncryptionPurpose.ANY).get(0); + + secretKeys = PGPainless.modifyKeyRing(secretKeys) + .revokeSubKey(encryptionSubkey.getKeyID(), protector, new RevocationSignatureSubpackets.Callback() { + @Override + public void modifyHashedSubpackets(RevocationSignatureSubpackets hashedSubpackets) { + hashedSubpackets.setRevocationReason( + RevocationAttributes.createKeyRevocation() + .withReason(RevocationAttributes.Reason.KEY_RETIRED) + .withDescription("I have a new Key.")); + // override issuer-fingerprint with null to test nulling of subpackets + hashedSubpackets.setIssuerFingerprint((IssuerFingerprint) null); + } + }) + .done(); + + encryptionSubkey = secretKeys.getPublicKey(encryptionSubkey.getKeyID()); + PGPSignature revocation = encryptionSubkey.getSignaturesOfType(SignatureType.SUBKEY_REVOCATION.getCode()).next(); + assertNotNull(revocation); + + assertNull(revocation.getHashedSubPackets().getIssuerFingerprint()); + assertEquals(secretKeys.getPublicKey().getKeyID(), + revocation.getHashedSubPackets().getIssuerKeyID()); + assertNotNull(SignatureSubpacketsUtil.getRevocationReason(revocation)); + assertFalse(SignatureUtils.isHardRevocation(revocation)); + } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/modification/RevokeUserIdsTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/modification/RevokeUserIdsTest.java new file mode 100644 index 00000000..fb6f1ec1 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/key/modification/RevokeUserIdsTest.java @@ -0,0 +1,102 @@ +// SPDX-FileCopyrightText: 2021 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.modification; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.util.NoSuchElementException; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.key.info.KeyRingInfo; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.key.util.RevocationAttributes; +import org.pgpainless.util.selection.userid.SelectUserId; + +public class RevokeUserIdsTest { + + @Test + public void revokeWithSelectUserId() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() + .modernKeyRing("Alice "); + SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); + + secretKeys = PGPainless.modifyKeyRing(secretKeys) + .addUserId("Allice ", protector) + .addUserId("Alice ", protector) + .done(); + + KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); + assertTrue(info.isUserIdValid("Alice ")); + assertTrue(info.isUserIdValid("Allice ")); + assertTrue(info.isUserIdValid("Alice ")); + + secretKeys = PGPainless.modifyKeyRing(secretKeys) + .revokeUserIds( + SelectUserId.containsEmailAddress("alice@example.org"), + protector, + RevocationAttributes.createCertificateRevocation() + .withReason(RevocationAttributes.Reason.USER_ID_NO_LONGER_VALID) + .withoutDescription()) + .done(); + + info = PGPainless.inspectKeyRing(secretKeys); + assertTrue(info.isUserIdValid("Alice ")); + assertFalse(info.isUserIdValid("Allice ")); + assertFalse(info.isUserIdValid("Alice ")); + } + + @Test + public void removeUserId() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() + .modernKeyRing("Alice "); + SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); + + secretKeys = PGPainless.modifyKeyRing(secretKeys) + .addUserId("Allice ", protector) + .addUserId("Alice ", protector) + .done(); + + KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); + assertTrue(info.isUserIdValid("Alice ")); + assertTrue(info.isUserIdValid("Allice ")); + assertTrue(info.isUserIdValid("Alice ")); + + secretKeys = PGPainless.modifyKeyRing(secretKeys) + .removeUserId("Allice ", protector) + .done(); + + info = PGPainless.inspectKeyRing(secretKeys); + assertTrue(info.isUserIdValid("Alice ")); + assertFalse(info.isUserIdValid("Allice ")); + assertTrue(info.isUserIdValid("Alice ")); + + PGPSignature revocation = info.getUserIdRevocation("Allice "); + assertNotNull(revocation); + + assertFalse(RevocationAttributes.Reason.isHardRevocation( + revocation.getHashedSubPackets().getRevocationReason().getRevocationReason())); + } + + @Test + public void emptySelectionYieldsNoSuchElementException() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() + .modernKeyRing("Alice "); + + assertThrows(NoSuchElementException.class, () -> + PGPainless.modifyKeyRing(secretKeys).revokeUserIds( + SelectUserId.containsEmailAddress("alice@example.org"), + SecretKeyRingProtector.unprotectedKeys(), + (RevocationAttributes) null)); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/modification/RevokedKeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/modification/RevokedKeyTest.java new file mode 100644 index 00000000..6082308d --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/key/modification/RevokedKeyTest.java @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.modification; + +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.key.info.KeyRingInfo; + +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +public class RevokedKeyTest { + + private static final String REVOKED = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "\n" + + "xjMEYumXWhYJKwYBBAHaRw8BAQdAsesa7C2dtchG2LDYRPVgNiyXDDltTIW0\n" + + "7hbPKuklr+LCeAQgFgoACQUCYume7wIdAAAhCRARRbJWkH7x7hYhBCHmM1W/\n" + + "k8Vt/xDX4xFFslaQfvHusjoBAKeMumYgtr1uwbcNobWhojRjik+Uq7jER1Ph\n" + + "zrZPPwyaAP9NpV4//AB5BbwUgHMhCErD8L6GZEBOpCWYDgS00eKmCc0kVGVz\n" + + "dCBVc2VyIDx0ZXN0LnVzZXJAZmxvd2NyeXB0LnRlc3Q+wqcEExYIADgWIQQh\n" + + "5jNVv5PFbf8Q1+MRRbJWkH7x7gUCYumXWgIbAwULCQgHAgYVCgkICwIEFgID\n" + + "AQIeAQIXgAAhCRARRbJWkH7x7hYhBCHmM1W/k8Vt/xDX4xFFslaQfvHu0GUB\n" + + "AJ/FAi0K0YQ/gv9fO2EwSLH9imrXSxtfkzAyCQS32A/IAQDdqUfbABEoQvo2\n" + + "n1ktpVXroW3XPe3HlYFwSQzpVSHADc44BGLpl1oSCisGAQQBl1UBBQEBB0DJ\n" + + "8e0hG6v64O4P3qa9n8FxrkNoKS+J+fAW1Vzpf5tBUQMBCAfCjwQYFggAIBYh\n" + + "BCHmM1W/k8Vt/xDX4xFFslaQfvHuBQJi6ZdaAhsMACEJEBFFslaQfvHuFiEE\n" + + "IeYzVb+TxW3/ENfjEUWyVpB+8e51yAD/ewAe43L4bXYehVAKq+/CSfXEpYxU\n" + + "8kZv/mfA6nRfvOIA/iTx2uNw5NzC6TM5ZCBrXVxVGPmR9SwjnBHRmzVAmT8B\n" + + "=pY9e\n" + + "-----END PGP PUBLIC KEY BLOCK-----"; + + @Test + public void test() throws IOException { + PGPPublicKeyRing cert = PGPainless.readKeyRing().publicKeyRing(REVOKED); + KeyRingInfo info = PGPainless.inspectKeyRing(cert); + + assertFalse(info.isUsableForSigning()); + assertFalse(info.isUsableForEncryption()); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/parsing/KeyRingCollectionReaderTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/parsing/KeyRingCollectionReaderTest.java index 5c117652..552ae25f 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/parsing/KeyRingCollectionReaderTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/parsing/KeyRingCollectionReaderTest.java @@ -27,8 +27,8 @@ public class KeyRingCollectionReaderTest { @Test public void writeAndParseKeyRingCollections() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { // secret keys - PGPSecretKeyRing alice = PGPainless.generateKeyRing().modernKeyRing("Alice ", null); - PGPSecretKeyRing bob = PGPainless.generateKeyRing().modernKeyRing("Bob ", null); + PGPSecretKeyRing alice = PGPainless.generateKeyRing().modernKeyRing("Alice "); + PGPSecretKeyRing bob = PGPainless.generateKeyRing().modernKeyRing("Bob "); PGPSecretKeyRingCollection collection = KeyRingUtils.keyRingsToKeyRingCollection(alice, bob); String ascii = ArmorUtils.toAsciiArmoredString(collection); diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/parsing/KeyRingReaderTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/parsing/KeyRingReaderTest.java index b02f5beb..5ae6d9b2 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/parsing/KeyRingReaderTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/parsing/KeyRingReaderTest.java @@ -4,19 +4,16 @@ package org.pgpainless.key.parsing; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; -import java.net.URISyntaxException; -import java.net.URL; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; @@ -27,13 +24,16 @@ import org.bouncycastle.bcpg.ArmoredOutputStream; import org.bouncycastle.bcpg.BCPGOutputStream; import org.bouncycastle.bcpg.MarkerPacket; import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPKeyRing; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.PGPUtil; +import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.Test; +import org.opentest4j.TestAbortedException; import org.pgpainless.PGPainless; import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.OpenPgpV4Fingerprint; @@ -45,9 +45,22 @@ import org.pgpainless.util.TestUtils; class KeyRingReaderTest { + private InputStream requireResource(String resourceName) { + InputStream inputStream = getClass().getClassLoader().getResourceAsStream(resourceName); + if (inputStream == null) { + throw new TestAbortedException("Cannot read resource " + resourceName + ": InputStream is null."); + } + return inputStream; + } + + private byte[] readFromResource(String resourceName) throws IOException { + InputStream inputStream = requireResource(resourceName); + return Streams.readAll(inputStream); + } + @Test public void assertThatPGPUtilsDetectAsciiArmoredData() throws IOException, PGPException { - InputStream inputStream = getClass().getClassLoader().getResourceAsStream("pub_keys_10_pieces.asc"); + InputStream inputStream = requireResource("pub_keys_10_pieces.asc"); InputStream possiblyArmored = PGPUtil.getDecoderStream(PGPUtil.getDecoderStream(inputStream)); @@ -58,7 +71,7 @@ class KeyRingReaderTest { @Test void publicKeyRingCollectionFromStream() throws IOException, PGPException { - InputStream inputStream = getClass().getClassLoader().getResourceAsStream("pub_keys_10_pieces.asc"); + InputStream inputStream = requireResource("pub_keys_10_pieces.asc"); PGPPublicKeyRingCollection rings = PGPainless.readKeyRing().publicKeyRingCollection(inputStream); assertEquals(10, rings.size()); } @@ -83,52 +96,50 @@ class KeyRingReaderTest { } @Test - void publicKeyRingCollectionFromString() throws IOException, PGPException, URISyntaxException { - URL resource = getClass().getClassLoader().getResource("pub_keys_10_pieces.asc"); - String armoredString = new String(Files.readAllBytes(new File(resource.toURI()).toPath())); + void publicKeyRingCollectionFromString() throws IOException, PGPException { + String armoredString = new String(readFromResource("pub_keys_10_pieces.asc")); InputStream inputStream = new ByteArrayInputStream(armoredString.getBytes(StandardCharsets.UTF_8)); PGPPublicKeyRingCollection rings = PGPainless.readKeyRing().publicKeyRingCollection(inputStream); assertEquals(10, rings.size()); } @Test - void publicKeyRingCollectionFromBytes() throws IOException, PGPException, URISyntaxException { - URL resource = getClass().getClassLoader().getResource("pub_keys_10_pieces.asc"); - byte[] bytes = Files.readAllBytes(new File(resource.toURI()).toPath()); + void publicKeyRingCollectionFromBytes() throws IOException, PGPException { + byte[] bytes = readFromResource("pub_keys_10_pieces.asc"); InputStream byteArrayInputStream = new ByteArrayInputStream(bytes); PGPPublicKeyRingCollection rings = PGPainless.readKeyRing().publicKeyRingCollection(byteArrayInputStream); assertEquals(10, rings.size()); } /** - * One armored pub key + * One armored pub key. */ @Test - void parsePublicKeysSingleArmored() throws IOException, PGPException, URISyntaxException { + void parsePublicKeysSingleArmored() throws IOException, PGPException { assertEquals(1, getPgpPublicKeyRingsFromResource("single_pub_key_armored.asc").size()); } /** - * One binary pub key + * One binary pub key. */ @Test - void parsePublicKeysSingleBinary() throws IOException, PGPException, URISyntaxException { + void parsePublicKeysSingleBinary() throws IOException, PGPException { assertEquals(1, getPgpPublicKeyRingsFromResource("single_pub_key_binary.key").size()); } /** - * Many armored pub keys with a single -----BEGIN PGP PUBLIC KEY BLOCK-----...-----END PGP PUBLIC KEY BLOCK----- + * Many armored pub keys with a single -----BEGIN PGP PUBLIC KEY BLOCK-----...-----END PGP PUBLIC KEY BLOCK-----. */ @Test - void parsePublicKeysMultiplyArmoredSingleHeader() throws IOException, PGPException, URISyntaxException { + void parsePublicKeysMultiplyArmoredSingleHeader() throws IOException, PGPException { assertEquals(10, getPgpPublicKeyRingsFromResource("10_pub_keys_armored_single_header.asc").size()); } /** - * Many armored pub keys where each has own -----BEGIN PGP PUBLIC KEY BLOCK-----...-----END PGP PUBLIC KEY BLOCK----- + * Many armored pub keys where each has own -----BEGIN PGP PUBLIC KEY BLOCK-----...-----END PGP PUBLIC KEY BLOCK-----. */ @Test - void parsePublicKeysMultiplyArmoredOwnHeader() throws IOException, PGPException, URISyntaxException { + void parsePublicKeysMultiplyArmoredOwnHeader() throws IOException, PGPException { assertEquals(10, getPgpPublicKeyRingsFromResource("10_pub_keys_armored_own_header.asc").size()); } @@ -137,48 +148,48 @@ class KeyRingReaderTest { * Each of those blocks can have a different count of keys. */ @Test - void parsePublicKeysMultiplyArmoredOwnWithSingleHeader() throws IOException, PGPException, URISyntaxException { + void parsePublicKeysMultiplyArmoredOwnWithSingleHeader() throws IOException, PGPException { assertEquals(10, getPgpPublicKeyRingsFromResource("10_pub_keys_armored_own_with_single_header.asc").size()); } /** - * Many binary pub keys + * Many binary pub keys. */ @Test - void parsePublicKeysMultiplyBinary() throws IOException, PGPException, URISyntaxException { + void parsePublicKeysMultiplyBinary() throws IOException, PGPException { assertEquals(10, getPgpPublicKeyRingsFromResource("10_pub_keys_binary.key").size()); } /** - * One armored private key + * One armored private key. */ @Test - void parseSecretKeysSingleArmored() throws IOException, PGPException, URISyntaxException { + void parseSecretKeysSingleArmored() throws IOException, PGPException { assertEquals(1, getPgpSecretKeyRingsFromResource("single_prv_key_armored.asc").size()); } /** - * One binary private key + * One binary private key. */ @Test - void parseSecretKeysSingleBinary() throws IOException, PGPException, URISyntaxException { + void parseSecretKeysSingleBinary() throws IOException, PGPException { assertEquals(1, getPgpSecretKeyRingsFromResource("single_prv_key_binary.key").size()); } /** - * Many armored private keys with a single + * Many armored private keys with a single header. * -----BEGIN PGP PRIVATE KEY BLOCK-----...-----END PGP PRIVATE KEY BLOCK----- */ @Test - void parseSecretKeysMultiplyArmoredSingleHeader() throws IOException, PGPException, URISyntaxException { + void parseSecretKeysMultiplyArmoredSingleHeader() throws IOException, PGPException { assertEquals(10, getPgpSecretKeyRingsFromResource("10_prv_keys_armored_single_header.asc").size()); } /** - * Many armored private keys where each has own -----BEGIN PGP PRIVATE KEY BLOCK-----...-----END PGP PRIVATE KEY BLOCK----- + * Many armored private keys where each has own -----BEGIN PGP PRIVATE KEY BLOCK-----...-----END PGP PRIVATE KEY BLOCK-----. */ @Test - void parseSecretKeysMultiplyArmoredOwnHeader() throws IOException, PGPException, URISyntaxException { + void parseSecretKeysMultiplyArmoredOwnHeader() throws IOException, PGPException { assertEquals(10, getPgpSecretKeyRingsFromResource("10_prv_keys_armored_own_header.asc").size()); } @@ -187,66 +198,60 @@ class KeyRingReaderTest { * Each of those blocks can have a different count of keys. */ @Test - void parseSecretKeysMultiplyArmoredOwnWithSingleHeader() throws IOException, PGPException, URISyntaxException { + void parseSecretKeysMultiplyArmoredOwnWithSingleHeader() throws IOException, PGPException { assertEquals(10, getPgpSecretKeyRingsFromResource("10_prv_keys_armored_own_with_single_header.asc").size()); } /** - * Many binary private keys + * Many binary private keys. */ @Test - void parseSecretKeysMultiplyBinary() throws IOException, PGPException, URISyntaxException { + void parseSecretKeysMultiplyBinary() throws IOException, PGPException { assertEquals(10, getPgpSecretKeyRingsFromResource("10_prv_keys_binary.key").size()); } /** - * Many armored keys(private or pub) where each has own -----BEGIN PGP ... KEY BLOCK-----...-----END PGP ... KEY BLOCK----- + * Many armored keys(private or pub) where each has own -----BEGIN PGP ... KEY BLOCK-----...-----END PGP ... KEY BLOCK-----. */ @Test - void parseKeysMultiplyArmoredOwnHeader() throws IOException, PGPException, URISyntaxException { + void parseKeysMultiplyArmoredOwnHeader() throws IOException, PGPException { assertEquals(10, getPGPKeyRingsFromResource("10_prv_and_pub_keys_armored_own_header.asc").size()); } /** - * Many armored keys(private or pub) where each has own -----BEGIN PGP ... KEY BLOCK-----...-----END PGP ... KEY BLOCK----- + * Many armored keys(private or pub) where each has own -----BEGIN PGP ... KEY BLOCK-----...-----END PGP ... KEY BLOCK-----. * Each of those blocks can have a different count of keys. */ @Test - void parseKeysMultiplyArmoredOwnWithSingleHeader() throws IOException, PGPException, URISyntaxException { + void parseKeysMultiplyArmoredOwnWithSingleHeader() throws IOException, PGPException { assertEquals(10, getPGPKeyRingsFromResource("10_prv_and_pub_keys_armored_own_with_single_header.asc").size()); } /** - * Many binary keys(private or pub) + * Many binary keys(private or pub). */ @Test - void parseKeysMultiplyBinary() throws IOException, PGPException, URISyntaxException { + void parseKeysMultiplyBinary() throws IOException, PGPException { assertEquals(10, getPGPKeyRingsFromResource("10_prv_and_pub_keys_binary.key").size()); } - private InputStream getFileInputStreamFromResource(String fileName) throws IOException, URISyntaxException { - URL resource = getClass().getClassLoader().getResource(fileName); - assert resource != null; - return new FileInputStream(new File(resource.toURI())); - } - private PGPKeyRingCollection getPGPKeyRingsFromResource(String fileName) - throws IOException, URISyntaxException, PGPException { - return PGPainless.readKeyRing().keyRingCollection(getFileInputStreamFromResource(fileName), true); + throws IOException, PGPException { + return PGPainless.readKeyRing().keyRingCollection(requireResource(fileName), true); } private PGPPublicKeyRingCollection getPgpPublicKeyRingsFromResource(String fileName) - throws IOException, URISyntaxException, PGPException { - return PGPainless.readKeyRing().publicKeyRingCollection(getFileInputStreamFromResource(fileName)); + throws IOException, PGPException { + return PGPainless.readKeyRing().publicKeyRingCollection(requireResource(fileName)); } private PGPSecretKeyRingCollection getPgpSecretKeyRingsFromResource(String fileName) - throws IOException, URISyntaxException, PGPException { - return PGPainless.readKeyRing().secretKeyRingCollection(getFileInputStreamFromResource(fileName)); + throws IOException, PGPException { + return PGPainless.readKeyRing().secretKeyRingCollection(requireResource(fileName)); } @Test - public void testReadSecretKeyIgnoresMarkerPacket() throws PGPException, IOException { + public void testReadSecretKeyIgnoresMarkerPacket() throws IOException { String markerAndKey = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + "Version: PGPainless\n" + "Comment: Secret Key with prepended Marker Packet\n" + @@ -445,8 +450,8 @@ class KeyRingReaderTest { @Test public void testReadSecretKeysIgnoresMultipleMarkers() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { - PGPSecretKeyRing alice = PGPainless.generateKeyRing().modernKeyRing("alice@pgpainless.org", null); - PGPSecretKeyRing bob = PGPainless.generateKeyRing().modernKeyRing("bob@pgpainless.org", null); + PGPSecretKeyRing alice = PGPainless.generateKeyRing().modernKeyRing("alice@pgpainless.org"); + PGPSecretKeyRing bob = PGPainless.generateKeyRing().modernKeyRing("bob@pgpainless.org"); MarkerPacket marker = TestUtils.getMarkerPacket(); ByteArrayOutputStream bytes = new ByteArrayOutputStream(); @@ -475,4 +480,138 @@ class KeyRingReaderTest { assertTrue(secretKeys.contains(alice.getSecretKey().getKeyID())); assertTrue(secretKeys.contains(bob.getSecretKey().getKeyID())); } + + @Test + public void testReadingSecretKeysExceedsIterationLimit() + throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + PGPSecretKeyRing alice = PGPainless.generateKeyRing().modernKeyRing("alice@pgpainless.org"); + MarkerPacket marker = TestUtils.getMarkerPacket(); + + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + ArmoredOutputStream armor = ArmoredOutputStreamFactory.get(bytes); + BCPGOutputStream outputStream = new BCPGOutputStream(armor); + + for (int i = 0; i < 600; i++) { + marker.encode(outputStream); + } + alice.encode(outputStream); + + assertThrows(IOException.class, () -> + KeyRingReader.readSecretKeyRing(new ByteArrayInputStream(bytes.toByteArray()), 512)); + } + + @Test + public void testReadingSecretKeyCollectionExceedsIterationLimit() + throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + PGPSecretKeyRing alice = PGPainless.generateKeyRing().modernKeyRing("alice@pgpainless.org"); + PGPSecretKeyRing bob = PGPainless.generateKeyRing().modernKeyRing("bob@pgpainless.org"); + MarkerPacket marker = TestUtils.getMarkerPacket(); + + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + ArmoredOutputStream armor = ArmoredOutputStreamFactory.get(bytes); + BCPGOutputStream outputStream = new BCPGOutputStream(armor); + + for (int i = 0; i < 600; i++) { + marker.encode(outputStream); + } + alice.encode(outputStream); + bob.encode(outputStream); + + assertThrows(IOException.class, () -> + KeyRingReader.readSecretKeyRingCollection(new ByteArrayInputStream(bytes.toByteArray()), 512)); + } + + + @Test + public void testReadingPublicKeysExceedsIterationLimit() + throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().modernKeyRing("alice@pgpainless.org"); + PGPPublicKeyRing alice = PGPainless.extractCertificate(secretKeys); + MarkerPacket marker = TestUtils.getMarkerPacket(); + + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + ArmoredOutputStream armor = ArmoredOutputStreamFactory.get(bytes); + BCPGOutputStream outputStream = new BCPGOutputStream(armor); + + for (int i = 0; i < 600; i++) { + marker.encode(outputStream); + } + alice.encode(outputStream); + + assertThrows(IOException.class, () -> + KeyRingReader.readPublicKeyRing(new ByteArrayInputStream(bytes.toByteArray()), 512)); + } + + @Test + public void testReadingPublicKeyCollectionExceedsIterationLimit() + throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + PGPSecretKeyRing sec1 = PGPainless.generateKeyRing().modernKeyRing("alice@pgpainless.org"); + PGPSecretKeyRing sec2 = PGPainless.generateKeyRing().modernKeyRing("bob@pgpainless.org"); + PGPPublicKeyRing alice = PGPainless.extractCertificate(sec1); + PGPPublicKeyRing bob = PGPainless.extractCertificate(sec2); + MarkerPacket marker = TestUtils.getMarkerPacket(); + + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + ArmoredOutputStream armor = ArmoredOutputStreamFactory.get(bytes); + BCPGOutputStream outputStream = new BCPGOutputStream(armor); + + for (int i = 0; i < 600; i++) { + marker.encode(outputStream); + } + alice.encode(outputStream); + bob.encode(outputStream); + + assertThrows(IOException.class, () -> + KeyRingReader.readPublicKeyRingCollection(new ByteArrayInputStream(bytes.toByteArray()), 512)); + } + + @Test + public void testReadKeyRingWithBinaryPublicKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().modernKeyRing("Alice "); + PGPPublicKeyRing publicKeys = PGPainless.extractCertificate(secretKeys); + byte[] bytes = publicKeys.getEncoded(); + + PGPKeyRing keyRing = PGPainless.readKeyRing() + .keyRing(bytes); + + assertTrue(keyRing instanceof PGPPublicKeyRing); + assertArrayEquals(keyRing.getEncoded(), publicKeys.getEncoded()); + } + + @Test + public void testReadKeyRingWithBinarySecretKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().modernKeyRing("Alice "); + byte[] bytes = secretKeys.getEncoded(); + + PGPKeyRing keyRing = PGPainless.readKeyRing() + .keyRing(bytes); + + assertTrue(keyRing instanceof PGPSecretKeyRing); + assertArrayEquals(keyRing.getEncoded(), secretKeys.getEncoded()); + } + + @Test + public void testReadKeyRingWithArmoredPublicKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().modernKeyRing("Alice "); + PGPPublicKeyRing publicKeys = PGPainless.extractCertificate(secretKeys); + String armored = PGPainless.asciiArmor(publicKeys); + + PGPKeyRing keyRing = PGPainless.readKeyRing() + .keyRing(armored); + + assertTrue(keyRing instanceof PGPPublicKeyRing); + assertArrayEquals(keyRing.getEncoded(), publicKeys.getEncoded()); + } + + @Test + public void testReadKeyRingWithArmoredSecretKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().modernKeyRing("Alice "); + String armored = PGPainless.asciiArmor(secretKeys); + + PGPKeyRing keyRing = PGPainless.readKeyRing() + .keyRing(armored); + + assertTrue(keyRing instanceof PGPSecretKeyRing); + assertArrayEquals(keyRing.getEncoded(), secretKeys.getEncoded()); + } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/protection/CachingSecretKeyRingProtectorTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/protection/CachingSecretKeyRingProtectorTest.java index 277d9d44..3f7a9e6e 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/protection/CachingSecretKeyRingProtectorTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/protection/CachingSecretKeyRingProtectorTest.java @@ -7,12 +7,13 @@ package org.pgpainless.key.protection; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import java.io.IOException; import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; import java.util.Iterator; import java.util.Random; -import javax.annotation.Nullable; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPKeyRing; @@ -22,6 +23,7 @@ import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; +import org.pgpainless.key.TestKeys; import org.pgpainless.key.protection.passphrase_provider.SecretKeyPassphraseProvider; import org.pgpainless.util.Passphrase; @@ -29,7 +31,6 @@ public class CachingSecretKeyRingProtectorTest { // Dummy passphrase callback that returns the doubled key-id as passphrase private final SecretKeyPassphraseProvider dummyCallback = new SecretKeyPassphraseProvider() { - @Nullable @Override public Passphrase getPassphraseFor(Long keyId) { long doubled = keyId * 2; @@ -128,7 +129,34 @@ public class CachingSecretKeyRingProtectorTest { Passphrase passphrase = withCallback.getPassphraseFor(x); assertNotNull(passphrase); - assertEquals(doubled, Long.valueOf(new String(passphrase.getChars()))); + assertNotNull(passphrase.getChars()); + assertEquals(doubled, Long.parseLong(new String(passphrase.getChars()))); + } + } + + @Test + public void testAddPassphrase_collision() throws PGPException, IOException { + PGPSecretKeyRing secretKeys = TestKeys.getCryptieSecretKeyRing(); + CachingSecretKeyRingProtector protector = new CachingSecretKeyRingProtector(); + protector.addPassphrase(secretKeys, TestKeys.CRYPTIE_PASSPHRASE); + + assertThrows(IllegalArgumentException.class, () -> + protector.addPassphrase(secretKeys.getPublicKey(), Passphrase.emptyPassphrase())); + + assertThrows(IllegalArgumentException.class, () -> + protector.addPassphrase(secretKeys, Passphrase.fromPassword("anotherPass"))); + } + + @Test + public void testReplacePassphrase() throws PGPException, IOException { + PGPSecretKeyRing secretKeys = TestKeys.getCryptieSecretKeyRing(); + CachingSecretKeyRingProtector protector = new CachingSecretKeyRingProtector(); + protector.addPassphrase(secretKeys, Passphrase.fromPassword("wrong")); + // no throwing + protector.replacePassphrase(secretKeys, TestKeys.CRYPTIE_PASSPHRASE); + + for (PGPSecretKey key : secretKeys) { + UnlockSecretKey.unlockSecretKey(key, protector); } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/protection/InvalidProtectionSettingsTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/protection/InvalidProtectionSettingsTest.java new file mode 100644 index 00000000..b0746398 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/key/protection/InvalidProtectionSettingsTest.java @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.protection; + +import org.junit.jupiter.api.Test; +import org.pgpainless.algorithm.HashAlgorithm; +import org.pgpainless.algorithm.SymmetricKeyAlgorithm; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class InvalidProtectionSettingsTest { + + @Test + public void unencryptedKeyRingProtectionSettingsThrows() { + assertThrows(IllegalArgumentException.class, () -> + new KeyRingProtectionSettings(SymmetricKeyAlgorithm.NULL, HashAlgorithm.SHA256, 0x60)); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/protection/PassphraseProtectedKeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/protection/PassphraseProtectedKeyTest.java index 45aaf20f..370cfd85 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/protection/PassphraseProtectedKeyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/protection/PassphraseProtectedKeyTest.java @@ -32,7 +32,7 @@ public class PassphraseProtectedKeyTest { @Nullable @Override public Passphrase getPassphraseFor(Long keyId) { - if (keyId.equals(TestKeys.CRYPTIE_KEY_ID)) { + if (keyId == TestKeys.CRYPTIE_KEY_ID) { return new Passphrase(TestKeys.CRYPTIE_PASSWORD.toCharArray()); } else { return null; @@ -41,7 +41,7 @@ public class PassphraseProtectedKeyTest { @Override public boolean hasPassphrase(Long keyId) { - return keyId.equals(TestKeys.CRYPTIE_KEY_ID); + return keyId == TestKeys.CRYPTIE_KEY_ID; } }); diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/protection/PassphraseTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/protection/PassphraseTest.java index 94eb5863..83477b28 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/protection/PassphraseTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/protection/PassphraseTest.java @@ -13,6 +13,8 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.pgpainless.util.Passphrase; public class PassphraseTest { @@ -49,15 +51,13 @@ public class PassphraseTest { assertTrue(fromEmptyChars.isEmpty()); } - @Test - public void testEmptyPassphrase() { - Passphrase empty = Passphrase.emptyPassphrase(); - assertNull(empty.getChars()); - assertTrue(empty.isEmpty()); + @ParameterizedTest + @ValueSource(strings = {"", " ", " ", "\t", "\t\t"}) + public void testEmptyPassphrases(String empty) { + Passphrase passphrase = Passphrase.fromPassword(empty); + assertTrue(passphrase.isEmpty()); - Passphrase trimmedEmpty = Passphrase.fromPassword(" "); - assertNull(trimmedEmpty.getChars()); - assertTrue(trimmedEmpty.isEmpty()); + assertEquals(Passphrase.emptyPassphrase(), passphrase); } @Test diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/protection/SecretKeyRingProtectorTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/protection/SecretKeyRingProtectorTest.java index 1a8a3231..a5030f74 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/protection/SecretKeyRingProtectorTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/protection/SecretKeyRingProtectorTest.java @@ -17,30 +17,29 @@ import java.util.Map; import java.util.Random; import java.util.concurrent.ConcurrentHashMap; -import javax.annotation.Nullable; - import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; import org.pgpainless.PGPainless; -import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.TestKeys; import org.pgpainless.key.protection.passphrase_provider.SecretKeyPassphraseProvider; import org.pgpainless.util.Passphrase; +import org.pgpainless.util.TestAllImplementations; public class SecretKeyRingProtectorTest { - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void testUnlockAllKeysWithSamePassword(ImplementationFactory implementationFactory) throws IOException, PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void testUnlockAllKeysWithSamePassword() + throws IOException, PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { PGPSecretKeyRing secretKeys = TestKeys.getCryptieSecretKeyRing(); - SecretKeyRingProtector protector = SecretKeyRingProtector.unlockAllKeysWith(TestKeys.CRYPTIE_PASSPHRASE, secretKeys); + SecretKeyRingProtector protector = + SecretKeyRingProtector.unlockEachKeyWith(TestKeys.CRYPTIE_PASSPHRASE, secretKeys); for (PGPSecretKey secretKey : secretKeys) { PBESecretKeyDecryptor decryptor = protector.getDecryptor(secretKey.getKeyID()); assertNotNull(decryptor); @@ -51,7 +50,8 @@ public class SecretKeyRingProtectorTest { for (PGPSecretKey unrelatedKey : unrelatedKeys) { PBESecretKeyDecryptor decryptor = protector.getDecryptor(unrelatedKey.getKeyID()); assertNull(decryptor); - assertThrows(PGPException.class, () -> unrelatedKey.extractPrivateKey(protector.getDecryptor(unrelatedKey.getKeyID()))); + assertThrows(PGPException.class, + () -> unrelatedKey.extractPrivateKey(protector.getDecryptor(unrelatedKey.getKeyID()))); } } @@ -66,17 +66,18 @@ public class SecretKeyRingProtectorTest { } } - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void testUnlockSingleKeyWithPassphrase(ImplementationFactory implementationFactory) throws IOException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void testUnlockSingleKeyWithPassphrase() + throws IOException, PGPException { PGPSecretKeyRing secretKeys = TestKeys.getCryptieSecretKeyRing(); Iterator iterator = secretKeys.iterator(); PGPSecretKey secretKey = iterator.next(); PGPSecretKey subKey = iterator.next(); - SecretKeyRingProtector protector = SecretKeyRingProtector.unlockSingleKeyWith(TestKeys.CRYPTIE_PASSPHRASE, secretKey); + SecretKeyRingProtector protector = + SecretKeyRingProtector.unlockSingleKeyWith(TestKeys.CRYPTIE_PASSPHRASE, secretKey); assertNotNull(protector.getDecryptor(secretKey.getKeyID())); assertNotNull(protector.getEncryptor(secretKey.getKeyID())); assertNull(protector.getEncryptor(subKey.getKeyID())); @@ -87,7 +88,8 @@ public class SecretKeyRingProtectorTest { public void testFromPassphraseMap() { Map passphraseMap = new ConcurrentHashMap<>(); passphraseMap.put(1L, Passphrase.emptyPassphrase()); - CachingSecretKeyRingProtector protector = (CachingSecretKeyRingProtector) SecretKeyRingProtector.fromPassphraseMap(passphraseMap); + CachingSecretKeyRingProtector protector = + (CachingSecretKeyRingProtector) SecretKeyRingProtector.fromPassphraseMap(passphraseMap); assertNotNull(protector.getPassphraseFor(1L)); assertNull(protector.getPassphraseFor(5L)); @@ -105,7 +107,6 @@ public class SecretKeyRingProtectorTest { passphraseMap.put(1L, Passphrase.emptyPassphrase()); CachingSecretKeyRingProtector protector = new CachingSecretKeyRingProtector(passphraseMap, KeyRingProtectionSettings.secureDefaultSettings(), new SecretKeyPassphraseProvider() { - @Nullable @Override public Passphrase getPassphraseFor(Long keyId) { return Passphrase.fromPassword("missingP455w0rd"); diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/protection/UnlockSecretKeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/protection/UnlockSecretKeyTest.java index 9e4c0984..c6a35ea9 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/protection/UnlockSecretKeyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/protection/UnlockSecretKeyTest.java @@ -27,12 +27,17 @@ public class UnlockSecretKeyTest { .simpleEcKeyRing("alice@wonderland.lit", "heureka!"); PGPSecretKey secretKey = secretKeyRing.getSecretKey(); - SecretKeyRingProtector correctPassphrase = SecretKeyRingProtector.unlockAllKeysWith(Passphrase.fromPassword("heureka!"), secretKeyRing); - SecretKeyRingProtector incorrectPassphrase = SecretKeyRingProtector.unlockAllKeysWith(Passphrase.fromPassword("bazinga!"), secretKeyRing); - SecretKeyRingProtector emptyPassphrase = SecretKeyRingProtector.unlockAllKeysWith(Passphrase.emptyPassphrase(), secretKeyRing); + SecretKeyRingProtector correctPassphrase = SecretKeyRingProtector + .unlockAnyKeyWith(Passphrase.fromPassword("heureka!")); + SecretKeyRingProtector incorrectPassphrase = SecretKeyRingProtector + .unlockAnyKeyWith(Passphrase.fromPassword("bazinga!")); + SecretKeyRingProtector emptyPassphrase = SecretKeyRingProtector + .unlockAnyKeyWith(Passphrase.emptyPassphrase()); Passphrase cleared = Passphrase.fromPassword("cleared"); cleared.clear(); - SecretKeyRingProtector invalidPassphrase = SecretKeyRingProtector.unlockAllKeysWith(cleared, secretKeyRing); + SecretKeyRingProtector invalidPassphrase = SecretKeyRingProtector + .unlockAnyKeyWith(cleared); + // Correct passphrase works PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKey, correctPassphrase); assertNotNull(privateKey); @@ -41,7 +46,7 @@ public class UnlockSecretKeyTest { UnlockSecretKey.unlockSecretKey(secretKey, incorrectPassphrase)); assertThrows(WrongPassphraseException.class, () -> UnlockSecretKey.unlockSecretKey(secretKey, emptyPassphrase)); - assertThrows(WrongPassphraseException.class, () -> + assertThrows(IllegalStateException.class, () -> UnlockSecretKey.unlockSecretKey(secretKey, invalidPassphrase)); } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/protection/UnprotectedKeysProtectorTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/protection/UnprotectedKeysProtectorTest.java index 76a90915..07f65a59 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/protection/UnprotectedKeysProtectorTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/protection/UnprotectedKeysProtectorTest.java @@ -10,7 +10,7 @@ import org.junit.jupiter.api.Test; public class UnprotectedKeysProtectorTest { - private UnprotectedKeysProtector protector = new UnprotectedKeysProtector(); + private final UnprotectedKeysProtector protector = new UnprotectedKeysProtector(); @Test public void testKeyProtectorReturnsNullDecryptor() { diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/protection/fixes/S2KUsageFixTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/protection/fixes/S2KUsageFixTest.java index d5956da4..ba6673e5 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/protection/fixes/S2KUsageFixTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/protection/fixes/S2KUsageFixTest.java @@ -67,7 +67,8 @@ public class S2KUsageFixTest { private static final String MESSAGE_PLAIN = "Hello, World!\n"; @Test - public void verifyOutFixInChangePassphraseWorks() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + public void verifyOutFixInChangePassphraseWorks() + throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { PGPSecretKeyRing before = PGPainless.generateKeyRing().modernKeyRing("Alice", "before"); for (PGPSecretKey key : before) { assertEquals(SecretKeyPacket.USAGE_SHA1, key.getS2KUsage()); @@ -83,7 +84,7 @@ public class S2KUsageFixTest { } PGPSecretKeyRing after = PGPainless.modifyKeyRing(unprotected) - .changePassphraseFromOldPassphrase(null) + .changePassphraseFromOldPassphrase(Passphrase.emptyPassphrase()) .withSecureDefaultSettings() .toNewPassphrase(Passphrase.fromPassword("after")) .done(); @@ -93,9 +94,10 @@ public class S2KUsageFixTest { } @Test - public void testFixS2KUsageFrom_USAGE_CHECKSUM_to_USAGE_SHA1() throws IOException, PGPException { + public void testFixS2KUsageFrom_USAGE_CHECKSUM_to_USAGE_SHA1() + throws IOException, PGPException { PGPSecretKeyRing keys = PGPainless.readKeyRing().secretKeyRing(KEY_WITH_USAGE_CHECKSUM); - SecretKeyRingProtector protector = SecretKeyRingProtector.unlockAllKeysWith(Passphrase.fromPassword("after"), keys); + SecretKeyRingProtector protector = SecretKeyRingProtector.unlockAnyKeyWith(Passphrase.fromPassword("after")); PGPSecretKeyRing fixed = S2KUsageFix.replaceUsageChecksumWithUsageSha1(keys, protector); for (PGPSecretKey key : fixed) { @@ -105,7 +107,8 @@ public class S2KUsageFixTest { testCanStillDecrypt(keys, protector); } - private void testCanStillDecrypt(PGPSecretKeyRing keys, SecretKeyRingProtector protector) throws PGPException, IOException { + private void testCanStillDecrypt(PGPSecretKeyRing keys, SecretKeyRingProtector protector) + throws PGPException, IOException { ByteArrayInputStream in = new ByteArrayInputStream(MESSAGE.getBytes(StandardCharsets.UTF_8)); DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() .onInputStream(in) diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/util/KeyIdUtilTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/util/KeyIdUtilTest.java new file mode 100644 index 00000000..f85bd43f --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/key/util/KeyIdUtilTest.java @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.util; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class KeyIdUtilTest { + + @Test + public void testParsing() { + String longKeyId = "b1bd1f049ec87f3d"; // we parse from lowercase, but formatting will produce uppercase + long asLong = KeyIdUtil.fromLongKeyId(longKeyId); + assertEquals(-5639317053693722819L, asLong); + assertEquals(longKeyId.toUpperCase(), KeyIdUtil.formatKeyId(-5639317053693722819L)); + } + + @Test + public void testParsingLowerAndUppercase() { + long fromLower = KeyIdUtil.fromLongKeyId("f5ffdf6d71dd5789"); + assertEquals(-720611754201229431L, fromLower); + long fromUpper = KeyIdUtil.fromLongKeyId("F5FFDF6D71DD5789"); + assertEquals(-720611754201229431L, fromUpper); + } + + @Test + public void formatLowerAsUpper() { + assertEquals("5F04ACF44FD822B1", KeyIdUtil.formatKeyId(KeyIdUtil.fromLongKeyId("5f04acf44fd822b1"))); + } + + @Test + public void testParsing0() { + long asLong = 0L; + String formatted = KeyIdUtil.formatKeyId(asLong); + assertEquals("0000000000000000", formatted); + assertEquals(asLong, KeyIdUtil.fromLongKeyId("0000000000000000")); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/util/KeyRingUtilTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/util/KeyRingUtilTest.java new file mode 100644 index 00000000..11fd5cd3 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/key/util/KeyRingUtilTest.java @@ -0,0 +1,91 @@ +// SPDX-FileCopyrightText: 2021 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.util; + +import org.bouncycastle.bcpg.attr.ImageAttribute; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPKeyPair; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.PGPSignatureGenerator; +import org.bouncycastle.openpgp.PGPUserAttributeSubpacketVector; +import org.bouncycastle.openpgp.PGPUserAttributeSubpacketVectorGenerator; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.HashAlgorithm; +import org.pgpainless.algorithm.KeyFlag; +import org.pgpainless.algorithm.SignatureType; +import org.pgpainless.implementation.ImplementationFactory; +import org.pgpainless.key.generation.KeyRingBuilder; +import org.pgpainless.key.generation.KeySpec; +import org.pgpainless.key.generation.type.KeyType; +import org.pgpainless.key.generation.type.ecc.EllipticCurve; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.key.protection.UnlockSecretKey; +import org.pgpainless.util.CollectionUtils; + +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.util.Random; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class KeyRingUtilTest { + + @Test + public void testInjectCertification() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() + .modernKeyRing("Alice"); + + // test preconditions + assertFalse(secretKeys.getPublicKey().getUserAttributes().hasNext()); + int sigCount = CollectionUtils.iteratorToList(secretKeys.getPublicKey().getSignatures()).size(); + + // Create "image" + byte[] image = new byte[512]; + new Random().nextBytes(image); + PGPUserAttributeSubpacketVectorGenerator userAttrGen = new PGPUserAttributeSubpacketVectorGenerator(); + userAttrGen.setImageAttribute(ImageAttribute.JPEG, image); + PGPUserAttributeSubpacketVector userAttr = userAttrGen.generate(); + + // create sig + PGPSignatureGenerator sigGen = new PGPSignatureGenerator( + ImplementationFactory.getInstance().getPGPContentSignerBuilder( + secretKeys.getPublicKey().getAlgorithm(), HashAlgorithm.SHA512.getAlgorithmId() + )); + sigGen.init( + SignatureType.POSITIVE_CERTIFICATION.getCode(), + UnlockSecretKey.unlockSecretKey(secretKeys.getSecretKey(), SecretKeyRingProtector.unprotectedKeys())); + PGPSignature signature = sigGen.generateCertification(userAttr, secretKeys.getPublicKey()); + // inject sig + secretKeys = KeyRingUtils.injectCertification(secretKeys, userAttr, signature); + + assertTrue(secretKeys.getPublicKey().getUserAttributes().hasNext()); + assertEquals(userAttr, secretKeys.getPublicKey().getUserAttributes().next()); + assertEquals(sigCount + 1, CollectionUtils.iteratorToList(secretKeys.getPublicKey().getSignatures()).size()); + } + + @Test + public void testKeysPlusPublicKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().modernKeyRing("Alice"); + PGPPublicKeyRing publicKeys = PGPainless.extractCertificate(secretKeys); + + PGPKeyPair keyPair = KeyRingBuilder.generateKeyPair(KeySpec.getBuilder( + KeyType.ECDH(EllipticCurve._P256), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE).build()); + PGPPublicKey pubkey = keyPair.getPublicKey(); + assertFalse(pubkey.isMasterKey()); + + PGPSecretKeyRing secretKeysPlus = KeyRingUtils.keysPlusPublicKey(secretKeys, pubkey); + assertNotNull(secretKeysPlus.getPublicKey(pubkey.getKeyID())); + + PGPPublicKeyRing publicKeysPlus = KeyRingUtils.keysPlusPublicKey(publicKeys, pubkey); + assertNotNull(publicKeysPlus.getPublicKey(pubkey.getKeyID())); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/util/RevocationAttributesTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/util/RevocationAttributesTest.java new file mode 100644 index 00000000..8f588a28 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/key/util/RevocationAttributesTest.java @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: 2021 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.util; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class RevocationAttributesTest { + + @Test + public void testIsHardRevocationReason() { + // No reason and key compromised are hard revocation reasons + assertTrue(RevocationAttributes.Reason.isHardRevocation(RevocationAttributes.Reason.NO_REASON)); + assertTrue(RevocationAttributes.Reason.isHardRevocation(RevocationAttributes.Reason.KEY_COMPROMISED)); + + // others are soft + assertFalse(RevocationAttributes.Reason.isHardRevocation(RevocationAttributes.Reason.USER_ID_NO_LONGER_VALID)); + assertFalse(RevocationAttributes.Reason.isHardRevocation(RevocationAttributes.Reason.KEY_RETIRED)); + assertFalse(RevocationAttributes.Reason.isHardRevocation(RevocationAttributes.Reason.KEY_SUPERSEDED)); + } + + @Test + public void fromReasonCode() { + assertEquals(RevocationAttributes.Reason.NO_REASON, RevocationAttributes.Reason.fromCode((byte) 0)); + assertEquals(RevocationAttributes.Reason.KEY_SUPERSEDED, RevocationAttributes.Reason.fromCode((byte) 1)); + assertEquals(RevocationAttributes.Reason.KEY_COMPROMISED, RevocationAttributes.Reason.fromCode((byte) 2)); + assertEquals(RevocationAttributes.Reason.KEY_RETIRED, RevocationAttributes.Reason.fromCode((byte) 3)); + assertEquals(RevocationAttributes.Reason.USER_ID_NO_LONGER_VALID, RevocationAttributes.Reason.fromCode((byte) 32)); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/policy/PolicySetterTest.java b/pgpainless-core/src/test/java/org/pgpainless/policy/PolicySetterTest.java index 31092c28..6e90847d 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/policy/PolicySetterTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/policy/PolicySetterTest.java @@ -16,9 +16,15 @@ import org.pgpainless.algorithm.PublicKeyAlgorithm; public class PolicySetterTest { @Test - public void testSetSignatureHashAlgorithmPolicy_NullFails() { + public void testSetCertificationSignatureHashAlgorithmPolicy_NullFails() { Policy policy = Policy.getInstance(); - assertThrows(NullPointerException.class, () -> policy.setSignatureHashAlgorithmPolicy(null)); + assertThrows(NullPointerException.class, () -> policy.setCertificationSignatureHashAlgorithmPolicy(null)); + } + + @Test + public void testSetDataSignatureHashAlgorithmPolicy_NullFails() { + Policy policy = Policy.getInstance(); + assertThrows(NullPointerException.class, () -> policy.setDataSignatureHashAlgorithmPolicy(null)); } @Test diff --git a/pgpainless-core/src/test/java/org/pgpainless/policy/PolicyTest.java b/pgpainless-core/src/test/java/org/pgpainless/policy/PolicyTest.java index 6d86f8d9..9ff4df85 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/policy/PolicyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/policy/PolicyTest.java @@ -6,9 +6,13 @@ package org.pgpainless.policy; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -16,6 +20,7 @@ import org.pgpainless.algorithm.CompressionAlgorithm; import org.pgpainless.algorithm.HashAlgorithm; import org.pgpainless.algorithm.PublicKeyAlgorithm; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; +import org.pgpainless.util.DateUtil; public class PolicyTest { @@ -33,13 +38,25 @@ public class PolicyTest { policy.setSymmetricKeyDecryptionAlgorithmPolicy(new Policy.SymmetricKeyAlgorithmPolicy(SymmetricKeyAlgorithm.AES_256, Arrays.asList(SymmetricKeyAlgorithm.AES_256, SymmetricKeyAlgorithm.AES_192, SymmetricKeyAlgorithm.AES_128, SymmetricKeyAlgorithm.BLOWFISH))); - policy.setSignatureHashAlgorithmPolicy(new Policy.HashAlgorithmPolicy(HashAlgorithm.SHA512, - Arrays.asList(HashAlgorithm.SHA512, HashAlgorithm.SHA384, HashAlgorithm.SHA256))); + Map sigHashAlgoMap = new HashMap<>(); + sigHashAlgoMap.put(HashAlgorithm.SHA512, null); + sigHashAlgoMap.put(HashAlgorithm.SHA384, null); + sigHashAlgoMap.put(HashAlgorithm.SHA256, null); + sigHashAlgoMap.put(HashAlgorithm.SHA224, null); + sigHashAlgoMap.put(HashAlgorithm.SHA1, DateUtil.parseUTCDate("2013-02-01 00:00:00 UTC")); + policy.setCertificationSignatureHashAlgorithmPolicy(new Policy.HashAlgorithmPolicy(HashAlgorithm.SHA512, sigHashAlgoMap)); + Map revHashAlgoMap = new HashMap<>(); + revHashAlgoMap.put(HashAlgorithm.SHA512, null); + revHashAlgoMap.put(HashAlgorithm.SHA384, null); + revHashAlgoMap.put(HashAlgorithm.SHA256, null); + revHashAlgoMap.put(HashAlgorithm.SHA224, null); + revHashAlgoMap.put(HashAlgorithm.SHA1, DateUtil.parseUTCDate("2013-02-01 00:00:00 UTC")); + revHashAlgoMap.put(HashAlgorithm.RIPEMD160, DateUtil.parseUTCDate("2013-02-01 00:00:00 UTC")); policy.setRevocationSignatureHashAlgorithmPolicy(new Policy.HashAlgorithmPolicy(HashAlgorithm.SHA512, - Arrays.asList(HashAlgorithm.SHA512, HashAlgorithm.SHA384, HashAlgorithm.SHA256, HashAlgorithm.SHA224, HashAlgorithm.SHA1))); + revHashAlgoMap)); - policy.setPublicKeyAlgorithmPolicy(Policy.PublicKeyAlgorithmPolicy.defaultPublicKeyAlgorithmPolicy()); + policy.setPublicKeyAlgorithmPolicy(Policy.PublicKeyAlgorithmPolicy.bsi2021PublicKeyAlgorithmPolicy()); } @Test @@ -90,31 +107,40 @@ public class PolicyTest { @Test public void testAcceptableSignatureHashAlgorithm() { - assertTrue(policy.getSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA512)); - assertTrue(policy.getSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA512.getAlgorithmId())); + assertTrue(policy.getCertificationSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA512)); + assertTrue(policy.getCertificationSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA512.getAlgorithmId())); + // Usage date before termination date -> acceptable + assertTrue(policy.getCertificationSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA1, DateUtil.parseUTCDate("2000-01-01 00:00:00 UTC"))); + assertTrue(policy.getCertificationSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA1.getAlgorithmId(), DateUtil.parseUTCDate("2000-01-01 00:00:00 UTC"))); } @Test public void testUnacceptableSignatureHashAlgorithm() { - assertFalse(policy.getSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA1)); - assertFalse(policy.getSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA1.getAlgorithmId())); + assertFalse(policy.getCertificationSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA1)); + assertFalse(policy.getCertificationSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA1.getAlgorithmId())); + assertFalse(policy.getCertificationSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA1, DateUtil.parseUTCDate("2020-01-01 00:00:00 UTC"))); + assertFalse(policy.getCertificationSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA1.getAlgorithmId(), DateUtil.parseUTCDate("2020-01-01 00:00:00 UTC"))); } @Test public void testDefaultSignatureHashAlgorithm() { - assertEquals(HashAlgorithm.SHA512, policy.getSignatureHashAlgorithmPolicy().defaultHashAlgorithm()); + assertEquals(HashAlgorithm.SHA512, policy.getCertificationSignatureHashAlgorithmPolicy().defaultHashAlgorithm()); } @Test public void testAcceptableRevocationSignatureHashAlgorithm() { assertTrue(policy.getRevocationSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA384)); assertTrue(policy.getRevocationSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA384.getAlgorithmId())); + assertTrue(policy.getCertificationSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA1, DateUtil.parseUTCDate("2000-01-01 00:00:00 UTC"))); + assertTrue(policy.getCertificationSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA1.getAlgorithmId(), DateUtil.parseUTCDate("2000-01-01 00:00:00 UTC"))); } @Test public void testUnacceptableRevocationSignatureHashAlgorithm() { assertFalse(policy.getRevocationSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.RIPEMD160)); assertFalse(policy.getRevocationSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.RIPEMD160.getAlgorithmId())); + assertFalse(policy.getCertificationSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA1, DateUtil.parseUTCDate("2020-01-01 00:00:00 UTC"))); + assertFalse(policy.getCertificationSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA1.getAlgorithmId(), DateUtil.parseUTCDate("2020-01-01 00:00:00 UTC"))); } @Test @@ -142,4 +168,41 @@ public class PolicyTest { policy.getNotationRegistry().addKnownNotation("notation@pgpainless.org"); assertTrue(policy.getNotationRegistry().isKnownNotation("notation@pgpainless.org")); } + + @Test + public void testUnknownSymmetricKeyEncryptionAlgorithmIsNotAcceptable() { + assertFalse(policy.getSymmetricKeyEncryptionAlgorithmPolicy().isAcceptable(-1)); + } + + @Test + public void testUnknownSymmetricKeyDecryptionAlgorithmIsNotAcceptable() { + assertFalse(policy.getSymmetricKeyDecryptionAlgorithmPolicy().isAcceptable(-1)); + } + + @Test + public void testUnknownSignatureHashAlgorithmIsNotAcceptable() { + assertFalse(policy.getCertificationSignatureHashAlgorithmPolicy().isAcceptable(-1)); + assertFalse(policy.getCertificationSignatureHashAlgorithmPolicy().isAcceptable(-1, new Date())); + } + + @Test + public void testUnknownRevocationHashAlgorithmIsNotAcceptable() { + assertFalse(policy.getRevocationSignatureHashAlgorithmPolicy().isAcceptable(-1)); + assertFalse(policy.getRevocationSignatureHashAlgorithmPolicy().isAcceptable(-1, new Date())); + } + + @Test + public void testUnknownCompressionAlgorithmIsNotAcceptable() { + assertFalse(policy.getCompressionAlgorithmPolicy().isAcceptable(-1)); + } + + @Test + public void testUnknownPublicKeyAlgorithmIsNotAcceptable() { + assertFalse(policy.getPublicKeyAlgorithmPolicy().isAcceptable(-1, 4096)); + } + + @Test + public void setNullSignerUserIdValidationLevelThrows() { + assertThrows(NullPointerException.class, () -> policy.setSignerUserIdValidationLevel(null)); + } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/policy/WeakRSAKeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/policy/WeakRSAKeyTest.java new file mode 100644 index 00000000..a2813402 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/policy/WeakRSAKeyTest.java @@ -0,0 +1,196 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.policy; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.DocumentSignatureType; +import org.pgpainless.decryption_verification.ConsumerOptions; +import org.pgpainless.decryption_verification.DecryptionStream; +import org.pgpainless.decryption_verification.MessageMetadata; +import org.pgpainless.encryption_signing.EncryptionOptions; +import org.pgpainless.encryption_signing.EncryptionStream; +import org.pgpainless.encryption_signing.ProducerOptions; +import org.pgpainless.encryption_signing.SigningOptions; +import org.pgpainless.exception.KeyException; +import org.pgpainless.key.generation.type.rsa.RsaLength; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.util.Passphrase; + +public class WeakRSAKeyTest { + + /** + * Test key. + * RSA-4096 CERTIFY_OTHERS + * - RSA-1024 SIGN_DATA + * - RSA-1024 ENCRYPT_COMMS, ENCRYPT_STORAGE + */ + private static final String WEAK_RSA_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 88FB 13F4 2DA6 8480 573D 200B 90DC 9A01 1496 8242\n" + + "Comment: Alice \n" + + "\n" + + "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\n" + + "=nxQa\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + + @Test + public void cannotGenerateWeakKeyWithDefaultPolicyTest() { + String userId = "Alice "; + assertThrows(IllegalArgumentException.class, () -> + PGPainless.generateKeyRing() + .rsaKeyRing(userId, RsaLength._1024, Passphrase.emptyPassphrase())); + } + + @Test + public void cannotSignWithWeakKey() throws IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(WEAK_RSA_KEY); + SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); + + SigningOptions signingOptions = SigningOptions.get(); + + assertThrows(KeyException.UnacceptableSigningKeyException.class, + () -> signingOptions.addInlineSignature( + protector, secretKeys, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT)); + } + + @Test + public void encryptDecryptRoundTripWithWeakRSAKey() throws IOException, PGPException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(WEAK_RSA_KEY); + PGPPublicKeyRing publicKeys = PGPainless.extractCertificate(secretKeys); + + ByteArrayOutputStream encryptOut = new ByteArrayOutputStream(); + EncryptionOptions encryptionOptions = EncryptionOptions.encryptCommunications() + .addRecipient(publicKeys); + + EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() + .onOutputStream(encryptOut) + .withOptions(ProducerOptions.encrypt(encryptionOptions)); + + encryptionStream.write("Hello, World!\n".getBytes(StandardCharsets.UTF_8)); + encryptionStream.close(); + + ByteArrayInputStream ciphertextIn = new ByteArrayInputStream(encryptOut.toByteArray()); + ByteArrayOutputStream plaintextOut = new ByteArrayOutputStream(); + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(ciphertextIn) + .withOptions(ConsumerOptions.get() + .addDecryptionKey(secretKeys)); + Streams.pipeAll(decryptionStream, plaintextOut); + decryptionStream.close(); + + MessageMetadata metadata = decryptionStream.getMetadata(); + assertTrue(metadata.isEncryptedFor(secretKeys)); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/provider/ProviderFactoryTest.java b/pgpainless-core/src/test/java/org/pgpainless/provider/ProviderFactoryTest.java index a7da6ca9..5489a11c 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/provider/ProviderFactoryTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/provider/ProviderFactoryTest.java @@ -8,10 +8,25 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import java.security.Provider; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; public class ProviderFactoryTest { + private final ProviderFactory customProviderFactory = new ProviderFactory() { + + @SuppressWarnings("deprecation") + final Provider provider = new Provider("PL", 1L, "PGPainlessTestProvider") { + + }; + + @Override + protected Provider getSecurityProvider() { + return provider; + } + + }; + @Test public void providerFactoryDefaultIsBouncyCastleTest() { assertEquals("BC", ProviderFactory.getProviderName()); @@ -21,25 +36,11 @@ public class ProviderFactoryTest { public void setCustomProviderTest() { ProviderFactory.setFactory(customProviderFactory); assertEquals("PL", ProviderFactory.getProviderName()); + } + + @AfterEach + public void resetToDefault() { // Reset back to BouncyCastle ProviderFactory.setFactory(new BouncyCastleProviderFactory()); } - - private ProviderFactory customProviderFactory = new ProviderFactory() { - - @SuppressWarnings("deprecation") - Provider provider = new Provider("PL", 1L, "PGPainlessTestProvider") { - - }; - - @Override - protected Provider _getProvider() { - return provider; - } - - @Override - protected String _getProviderName() { - return provider.getName(); - } - }; } diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/BindingSignatureSubpacketsTest.java b/pgpainless-core/src/test/java/org/pgpainless/signature/BindingSignatureSubpacketsTest.java index 01f5356b..331ca07a 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/signature/BindingSignatureSubpacketsTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/BindingSignatureSubpacketsTest.java @@ -16,12 +16,13 @@ import java.util.Date; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSignature; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; import org.pgpainless.PGPainless; import org.pgpainless.exception.SignatureValidationException; -import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.policy.Policy; +import org.pgpainless.signature.consumer.CertificateValidator; +import org.pgpainless.util.TestAllImplementations; /** * Explores how subpackets on binding sigs are handled. @@ -50,10 +51,9 @@ public class BindingSignatureSubpacketsTest { private Date validationDate = new Date(); private Policy policy = PGPainless.getPolicy(); - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void baseCase(ImplementationFactory implementationFactory) throws IOException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void baseCase() throws IOException, PGPException { String key = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + "\n" + @@ -112,10 +112,9 @@ public class BindingSignatureSubpacketsTest { expectSignatureValidationSucceeds(key, "Base case. Is valid."); } - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void subkeyBindingIssuerFpOnly(ImplementationFactory implementationFactory) throws IOException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void subkeyBindingIssuerFpOnly() throws IOException, PGPException { String key = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + "\n" + @@ -174,10 +173,9 @@ public class BindingSignatureSubpacketsTest { expectSignatureValidationSucceeds(key, "Interoperability concern."); } - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void subkeyBindingIssuerV6IssuerFp(ImplementationFactory implementationFactory) throws IOException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void subkeyBindingIssuerV6IssuerFp() throws IOException, PGPException { String key = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + "\n" + @@ -236,10 +234,9 @@ public class BindingSignatureSubpacketsTest { expectSignatureValidationSucceeds(key, "Interoperability concern"); } - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void subkeyBindingIssuerFakeIssuer(ImplementationFactory implementationFactory) throws IOException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void subkeyBindingIssuerFakeIssuer() throws IOException, PGPException { String key = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + "\n" + @@ -298,10 +295,9 @@ public class BindingSignatureSubpacketsTest { expectSignatureValidationSucceeds(key, "Interoperability concern."); } - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void subkeyBindingFakeIssuerIssuer(ImplementationFactory implementationFactory) throws IOException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void subkeyBindingFakeIssuerIssuer() throws IOException, PGPException { String key = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + "\n" + @@ -360,10 +356,9 @@ public class BindingSignatureSubpacketsTest { expectSignatureValidationSucceeds(key, "Interop concern"); } - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void subkeyBindingFakeIssuer(ImplementationFactory implementationFactory) throws IOException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void subkeyBindingFakeIssuer() throws IOException, PGPException { String key = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + "\n" + @@ -422,10 +417,9 @@ public class BindingSignatureSubpacketsTest { expectSignatureValidationSucceeds(key, "fake issuers do not throw us off here."); } - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void subkeyBindingNoIssuer(ImplementationFactory implementationFactory) throws IOException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void subkeyBindingNoIssuer() throws IOException, PGPException { String key = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + "\n" + @@ -483,10 +477,9 @@ public class BindingSignatureSubpacketsTest { expectSignatureValidationSucceeds(key, "subkey binding sig does not need issuer"); } - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void unknownSubpacketHashed(ImplementationFactory implementationFactory) throws IOException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void unknownSubpacketHashed() throws IOException, PGPException { String key = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + "\n" + @@ -545,10 +538,9 @@ public class BindingSignatureSubpacketsTest { expectSignatureValidationSucceeds(key, "Unknown subpackets are okay in hashed area"); } - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void subkeyBindingUnknownCriticalSubpacket(ImplementationFactory implementationFactory) throws IOException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void subkeyBindingUnknownCriticalSubpacket() throws IOException, PGPException { String key = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + "\n" + @@ -607,10 +599,9 @@ public class BindingSignatureSubpacketsTest { expectSignatureValidationFails(key, "Unknown critical subpacket in hashed area invalidates signature"); } - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void subkeyBindingUnknownSubpacketUnhashed(ImplementationFactory implementationFactory) throws IOException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void subkeyBindingUnknownSubpacketUnhashed() throws IOException, PGPException { String key = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + "\n" + @@ -669,10 +660,9 @@ public class BindingSignatureSubpacketsTest { expectSignatureValidationSucceeds(key, "Unknown subpackets may be allowed in unhashed area."); } - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void subkeyBindingUnknownCriticalSubpacketUnhashed(ImplementationFactory implementationFactory) throws IOException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void subkeyBindingUnknownCriticalSubpacketUnhashed() throws IOException, PGPException { String key = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + "\n" + @@ -731,10 +721,9 @@ public class BindingSignatureSubpacketsTest { expectSignatureValidationSucceeds(key, "Critical unknown subpacket is okay in unhashed area."); } - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void subkeyBindingUnknownNotationHashed(ImplementationFactory implementationFactory) throws IOException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void subkeyBindingUnknownNotationHashed() throws IOException, PGPException { String key = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + "\n" + @@ -794,10 +783,9 @@ public class BindingSignatureSubpacketsTest { expectSignatureValidationSucceeds(key, "Unknown notation is okay in subkey binding sig."); } - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void subkeyBindingCriticalUnknownNotationHashed(ImplementationFactory implementationFactory) throws IOException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void subkeyBindingCriticalUnknownNotationHashed() throws IOException, PGPException { String key = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + "\n" + @@ -857,10 +845,9 @@ public class BindingSignatureSubpacketsTest { expectSignatureValidationFails(key, "Critical unknown notation invalidates subkey binding sig."); } - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void subkeyBindingUnknownNotationUnhashed(ImplementationFactory implementationFactory) throws IOException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void subkeyBindingUnknownNotationUnhashed() throws IOException, PGPException { String key = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + "\n" + @@ -920,10 +907,9 @@ public class BindingSignatureSubpacketsTest { expectSignatureValidationSucceeds(key, "Unknown notation is okay in unhashed area."); } - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void subkeyBindingCriticalUnknownNotationUnhashed(ImplementationFactory implementationFactory) throws IOException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void subkeyBindingCriticalUnknownNotationUnhashed() throws IOException, PGPException { String key = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + "\n" + @@ -983,10 +969,9 @@ public class BindingSignatureSubpacketsTest { expectSignatureValidationSucceeds(key, "Critical unknown notation is okay in unhashed area."); } - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void subkeyBindingBackSigFakeBackSig(ImplementationFactory implementationFactory) throws IOException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void subkeyBindingBackSigFakeBackSig() throws IOException, PGPException { String key = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + "\n" + @@ -1056,10 +1041,9 @@ public class BindingSignatureSubpacketsTest { expectSignatureValidationSucceeds(key, "Back-sig, fake back-sig should succeed to verify"); } - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void subkeyBindingFakeBackSigBackSig(ImplementationFactory implementationFactory) throws IOException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void subkeyBindingFakeBackSigBackSig() throws IOException, PGPException { String key = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + "\n" + @@ -1129,10 +1113,9 @@ public class BindingSignatureSubpacketsTest { expectSignatureValidationSucceeds(key, "Fake back-sig, back-sig should succeed to verify."); } - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void primaryBindingIssuerFpOnly(ImplementationFactory implementationFactory) throws IOException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void primaryBindingIssuerFpOnly() throws IOException, PGPException { String key = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + "\n" + @@ -1191,10 +1174,9 @@ public class BindingSignatureSubpacketsTest { expectSignatureValidationSucceeds(key, "issuer fp is enough"); } - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void primaryBindingIssuerV6IssuerFp(ImplementationFactory implementationFactory) throws IOException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void primaryBindingIssuerV6IssuerFp() throws IOException, PGPException { String key = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + "\n" + @@ -1253,10 +1235,9 @@ public class BindingSignatureSubpacketsTest { expectSignatureValidationSucceeds(key, "interop"); } - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void primaryBindingIssuerFakeIssuer(ImplementationFactory implementationFactory) throws IOException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void primaryBindingIssuerFakeIssuer() throws IOException, PGPException { String key = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + "\n" + @@ -1315,10 +1296,9 @@ public class BindingSignatureSubpacketsTest { expectSignatureValidationSucceeds(key, "interop"); } - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void primaryBindingFakeIssuerIssuer(ImplementationFactory implementationFactory) throws IOException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void primaryBindingFakeIssuerIssuer() throws IOException, PGPException { String key = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + "\n" + @@ -1377,10 +1357,9 @@ public class BindingSignatureSubpacketsTest { expectSignatureValidationSucceeds(key, "interop"); } - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void primaryBindingFakeIssuer(ImplementationFactory implementationFactory) throws IOException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void primaryBindingFakeIssuer() throws IOException, PGPException { String key = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + "\n" + @@ -1439,10 +1418,9 @@ public class BindingSignatureSubpacketsTest { expectSignatureValidationSucceeds(key, "Fake issuer on primary key binding sig is not an issue."); } - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void primaryBindingNoIssuer(ImplementationFactory implementationFactory) throws IOException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void primaryBindingNoIssuer() throws IOException, PGPException { String key = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + "\n" + @@ -1500,10 +1478,9 @@ public class BindingSignatureSubpacketsTest { expectSignatureValidationSucceeds(key, "Missing issuer on primary key binding sig is not an issue"); } - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void primaryBindingUnknownSubpacketHashed(ImplementationFactory implementationFactory) throws IOException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void primaryBindingUnknownSubpacketHashed() throws IOException, PGPException { String key = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + "\n" + @@ -1562,10 +1539,9 @@ public class BindingSignatureSubpacketsTest { expectSignatureValidationSucceeds(key, "Unknown subpacket in hashed area is not a problem."); } - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void primaryBindingCriticalUnknownSubpacketHashed(ImplementationFactory implementationFactory) throws IOException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void primaryBindingCriticalUnknownSubpacketHashed() throws IOException, PGPException { String key = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + "\n" + @@ -1624,10 +1600,9 @@ public class BindingSignatureSubpacketsTest { expectSignatureValidationFails(key, "Critical unknown subpacket in hashed area invalidates signature."); } - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void primaryBindingUnknownSubpacketUnhashed(ImplementationFactory implementationFactory) throws IOException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void primaryBindingUnknownSubpacketUnhashed() throws IOException, PGPException { String key = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + "\n" + @@ -1686,10 +1661,9 @@ public class BindingSignatureSubpacketsTest { expectSignatureValidationSucceeds(key, "Unknown subpacket is not an issue in the unhashed area"); } - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void primaryBindingCriticalUnknownSubpacketUnhashed(ImplementationFactory implementationFactory) throws IOException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void primaryBindingCriticalUnknownSubpacketUnhashed() throws IOException, PGPException { String key = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + "\n" + @@ -1748,10 +1722,9 @@ public class BindingSignatureSubpacketsTest { expectSignatureValidationSucceeds(key, "Critical unknown subpacket is acceptable in unhashed area of primary binding sig"); } - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void primaryBindingUnknownNotationHashed(ImplementationFactory implementationFactory) throws IOException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void primaryBindingUnknownNotationHashed() throws IOException, PGPException { String key = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + "\n" + @@ -1811,10 +1784,9 @@ public class BindingSignatureSubpacketsTest { expectSignatureValidationSucceeds(key, "Unknown notation is acceptable in hashed area of primary binding sig."); } - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void primaryBindingCriticalUnknownNotationHashed(ImplementationFactory implementationFactory) throws IOException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void primaryBindingCriticalUnknownNotationHashed() throws IOException, PGPException { String key = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + "\n" + @@ -1874,10 +1846,9 @@ public class BindingSignatureSubpacketsTest { expectSignatureValidationFails(key, "Critical unknown notation in hashed area invalidates primary binding sig"); } - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void primaryBindingUnknownNotationUnhashed(ImplementationFactory implementationFactory) throws IOException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void primaryBindingUnknownNotationUnhashed() throws IOException, PGPException { String key = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + "\n" + @@ -1937,10 +1908,9 @@ public class BindingSignatureSubpacketsTest { expectSignatureValidationSucceeds(key, "Unknown notation in unhashed area of primary key binding is okay."); } - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void primaryBindingCriticalUnknownNotationUnhashed(ImplementationFactory implementationFactory) throws IOException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void primaryBindingCriticalUnknownNotationUnhashed() throws IOException, PGPException { String key = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + "\n" + diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/CertificateExpirationTest.java b/pgpainless-core/src/test/java/org/pgpainless/signature/CertificateExpirationTest.java index c87bd2aa..f8c6471e 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/signature/CertificateExpirationTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/CertificateExpirationTest.java @@ -21,6 +21,7 @@ import org.pgpainless.encryption_signing.EncryptionOptions; import org.pgpainless.encryption_signing.EncryptionResult; import org.pgpainless.encryption_signing.EncryptionStream; import org.pgpainless.encryption_signing.ProducerOptions; +import org.pgpainless.exception.KeyException; import org.pgpainless.key.OpenPgpV4Fingerprint; import org.pgpainless.key.SubkeyIdentifier; @@ -124,7 +125,7 @@ public class CertificateExpirationTest { "-----END PGP PUBLIC KEY BLOCK-----\n"; PGPPublicKeyRing cert = PGPainless.readKeyRing().publicKeyRing(CERT); - assertThrows(IllegalArgumentException.class, () -> encrypt(cert)); + assertThrows(KeyException.ExpiredKeyException.class, () -> encrypt(cert)); } private EncryptionResult encrypt(PGPPublicKeyRing certificate) throws PGPException, IOException { diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/CertificateValidatorTest.java b/pgpainless-core/src/test/java/org/pgpainless/signature/CertificateValidatorTest.java index e01bf84d..175ce101 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/signature/CertificateValidatorTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/CertificateValidatorTest.java @@ -19,15 +19,16 @@ import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; import org.pgpainless.PGPainless; import org.pgpainless.decryption_verification.ConsumerOptions; import org.pgpainless.decryption_verification.DecryptionStream; -import org.pgpainless.decryption_verification.OpenPgpMetadata; +import org.pgpainless.decryption_verification.MessageMetadata; import org.pgpainless.exception.SignatureValidationException; -import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.policy.Policy; +import org.pgpainless.signature.consumer.CertificateValidator; +import org.pgpainless.util.TestAllImplementations; public class CertificateValidatorTest { @@ -36,10 +37,9 @@ public class CertificateValidatorTest { * * @see Sequoia Test Suite */ - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void testPrimaryKeySignsAndIsHardRevokedUnknown(ImplementationFactory implementationFactory) throws IOException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void testPrimaryKeySignsAndIsHardRevokedUnknown() throws IOException, PGPException { String key = "-----BEGIN PGP ARMORED FILE-----\n" + "Comment: ASCII Armor added by openpgp-interoperability-test-suite\n" + @@ -188,10 +188,9 @@ public class CertificateValidatorTest { * Subkey signs, primary key is hard revoked with reason: unknown. * @see Sequoia Test Suite */ - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void testSubkeySignsPrimaryKeyIsHardRevokedUnknown(ImplementationFactory implementationFactory) throws IOException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void testSubkeySignsPrimaryKeyIsHardRevokedUnknown() throws IOException, PGPException { String key = "-----BEGIN PGP ARMORED FILE-----\n" + "Comment: ASCII Armor added by openpgp-interoperability-test-suite\n" + @@ -341,10 +340,9 @@ public class CertificateValidatorTest { * * @see Sequoia Test Suite */ - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void testSubkeySignsAndIsHardRevokedUnknown(ImplementationFactory implementationFactory) throws IOException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void testSubkeySignsAndIsHardRevokedUnknown() throws IOException, PGPException { String keyWithHardRev = "-----BEGIN PGP ARMORED FILE-----\n" + "Comment: ASCII Armor added by openpgp-interoperability-test-suite\n" + @@ -494,10 +492,9 @@ public class CertificateValidatorTest { * * @see Sequoia Test Suite */ - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void testPrimaryKeySignsAndIsSoftRevokedSuperseded(ImplementationFactory implementationFactory) throws IOException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void testPrimaryKeySignsAndIsSoftRevokedSuperseded() throws IOException, PGPException { String keyWithSoftRev = "-----BEGIN PGP ARMORED FILE-----\n" + "Comment: ASCII Armor added by openpgp-interoperability-test-suite\n" + @@ -652,10 +649,9 @@ public class CertificateValidatorTest { * * @see Sequoia Test Suite */ - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void testSubkeySignsPrimaryKeyIsSoftRevokedSuperseded(ImplementationFactory implementationFactory) throws IOException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void testSubkeySignsPrimaryKeyIsSoftRevokedSuperseded() throws IOException, PGPException { String key = "-----BEGIN PGP ARMORED FILE-----\n" + "Comment: ASCII Armor added by openpgp-interoperability-test-suite\n" + @@ -806,10 +802,9 @@ public class CertificateValidatorTest { * * @see Sequoia Test Suite */ - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void testPrimaryKeySignsAndIsSoftRevokedRetired(ImplementationFactory implementationFactory) throws IOException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void testPrimaryKeySignsAndIsSoftRevokedRetired() throws IOException, PGPException { String key = "-----BEGIN PGP ARMORED FILE-----\n" + "Comment: ASCII Armor added by openpgp-interoperability-test-suite\n" + @@ -960,10 +955,9 @@ public class CertificateValidatorTest { * * @see Sequoia Test Suite */ - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void testTemporaryValidity(ImplementationFactory implementationFactory) throws IOException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void testTemporaryValidity() throws IOException, PGPException { String keyA = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + "Comment: D1A6 6E1A 23B1 82C9 980F 788C FBFC C82A 015E 7330\n" + @@ -1397,7 +1391,7 @@ public class CertificateValidatorTest { Streams.drain(decryptionStream); decryptionStream.close(); - OpenPgpMetadata metadata = decryptionStream.getResult(); + MessageMetadata metadata = decryptionStream.getMetadata(); assertFalse(metadata.getVerifiedSignatures().isEmpty()); } diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/IgnoreMarkerPackets.java b/pgpainless-core/src/test/java/org/pgpainless/signature/IgnoreMarkerPacketsTest.java similarity index 97% rename from pgpainless-core/src/test/java/org/pgpainless/signature/IgnoreMarkerPackets.java rename to pgpainless-core/src/test/java/org/pgpainless/signature/IgnoreMarkerPacketsTest.java index c86efd05..ff26506d 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/signature/IgnoreMarkerPackets.java +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/IgnoreMarkerPacketsTest.java @@ -24,7 +24,7 @@ import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.decryption_verification.ConsumerOptions; import org.pgpainless.decryption_verification.DecryptionStream; -import org.pgpainless.decryption_verification.OpenPgpMetadata; +import org.pgpainless.decryption_verification.MessageMetadata; import org.pgpainless.key.OpenPgpV4Fingerprint; import org.pgpainless.key.util.KeyRingUtils; @@ -33,7 +33,7 @@ import org.pgpainless.key.util.KeyRingUtils; * * @see Sequoia Test-Suite */ -public class IgnoreMarkerPackets { +public class IgnoreMarkerPacketsTest { private static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + "Comment: Bob's OpenPGP Transferable Secret Key\n" + @@ -154,8 +154,8 @@ public class IgnoreMarkerPackets { Streams.pipeAll(decryptionStream, outputStream); decryptionStream.close(); - OpenPgpMetadata metadata = decryptionStream.getResult(); - assertTrue(metadata.containsVerifiedSignatureFrom(new OpenPgpV4Fingerprint("D1A66E1A23B182C9980F788CFBFCC82A015E7330"))); + MessageMetadata metadata = decryptionStream.getMetadata(); + assertTrue(metadata.isVerifiedSignedBy(new OpenPgpV4Fingerprint("D1A66E1A23B182C9980F788CFBFCC82A015E7330"))); } @Test @@ -204,8 +204,8 @@ public class IgnoreMarkerPackets { decryptionStream.close(); assertArrayEquals(data.getBytes(StandardCharsets.UTF_8), outputStream.toByteArray()); - OpenPgpMetadata metadata = decryptionStream.getResult(); - assertTrue(metadata.containsVerifiedSignatureFrom(new OpenPgpV4Fingerprint("D1A66E1A23B182C9980F788CFBFCC82A015E7330"))); + MessageMetadata metadata = decryptionStream.getMetadata(); + assertTrue(metadata.isVerifiedSignedBy(new OpenPgpV4Fingerprint("D1A66E1A23B182C9980F788CFBFCC82A015E7330"))); } @Test diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/KeyRevocationTest.java b/pgpainless-core/src/test/java/org/pgpainless/signature/KeyRevocationTest.java index c4f899b4..67f5cf4c 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/signature/KeyRevocationTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/KeyRevocationTest.java @@ -14,20 +14,20 @@ import java.util.Date; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSignature; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; import org.pgpainless.PGPainless; import org.pgpainless.exception.SignatureValidationException; -import org.pgpainless.implementation.ImplementationFactory; +import org.pgpainless.signature.consumer.CertificateValidator; +import org.pgpainless.util.TestAllImplementations; public class KeyRevocationTest { private static final String data = "Hello, World"; - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void subkeySignsPrimaryKeyRevokedNoReason(ImplementationFactory implementationFactory) throws IOException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void subkeySignsPrimaryKeyRevokedNoReason() throws IOException, PGPException { String key = "-----BEGIN PGP ARMORED FILE-----\n" + "Comment: ASCII Armor added by openpgp-interoperability-test-suite\n" + @@ -172,10 +172,9 @@ public class KeyRevocationTest { * * @see Sequoia Test-Suite */ - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void subkeySignsPrimaryKeyNotRevoked(ImplementationFactory implementationFactory) throws IOException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void subkeySignsPrimaryKeyNotRevoked() throws IOException, PGPException { String key = "-----BEGIN PGP ARMORED FILE-----\n" + "Comment: ASCII Armor added by openpgp-interoperability-test-suite\n" + diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/KeyRingValidationTest.java b/pgpainless-core/src/test/java/org/pgpainless/signature/KeyRingValidationTest.java deleted file mode 100644 index 0352abba..00000000 --- a/pgpainless-core/src/test/java/org/pgpainless/signature/KeyRingValidationTest.java +++ /dev/null @@ -1,125 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature; - -import java.io.IOException; -import java.util.Collections; -import java.util.Date; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.pgpainless.PGPainless; -import org.pgpainless.algorithm.HashAlgorithm; -import org.pgpainless.key.KeyRingValidator; -import org.pgpainless.policy.Policy; -import org.pgpainless.util.ArmorUtils; -import org.pgpainless.util.DateUtil; - -public class KeyRingValidationTest { - - private static Policy.HashAlgorithmPolicy defaultSignatureHashAlgorithmPolicy; - - @BeforeAll - public static void setCustomPolicy() { - Policy policy = PGPainless.getPolicy(); - defaultSignatureHashAlgorithmPolicy = policy.getSignatureHashAlgorithmPolicy(); - - policy.setSignatureHashAlgorithmPolicy(new Policy.HashAlgorithmPolicy(HashAlgorithm.SHA256, Collections.singletonList(HashAlgorithm.SHA256))); - } - - @AfterAll - public static void resetCustomPolicy() { - PGPainless.getPolicy().setSignatureHashAlgorithmPolicy(defaultSignatureHashAlgorithmPolicy); - } - - @Test - public void testSignatureValidationOnPrimaryKey() throws IOException, PGPException { - String key = "-----BEGIN PGP ARMORED FILE-----\n" + - "Comment: ASCII Armor added by openpgp-interoperability-test-suite\n" + - "\n" + - "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\n" + - "exdvhN4AXDlPlB3Rrkj7CQ==\n" + - "=qQpG\n" + - "-----END PGP ARMORED FILE-----\n"; - - PGPPublicKeyRing publicKeys = PGPainless.readKeyRing().publicKeyRing(key); - - Date validationDate = DateUtil.parseUTCDate("2019-05-01 00:00:00 UTC"); - Policy policy = PGPainless.getPolicy(); - PGPPublicKeyRing evaluated = KeyRingValidator.validate(publicKeys, policy, validationDate); - - // CHECKSTYLE:OFF - System.out.println(ArmorUtils.toAsciiArmoredString(evaluated)); - // CHECKSTYLE:ON - - - } -} diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/OnePassSignatureBracketingTest.java b/pgpainless-core/src/test/java/org/pgpainless/signature/OnePassSignatureBracketingTest.java index 2abf5efc..fd7c53e9 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/signature/OnePassSignatureBracketingTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/OnePassSignatureBracketingTest.java @@ -31,12 +31,10 @@ import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.PGPSignatureList; import org.bouncycastle.openpgp.PGPUtil; -import org.bouncycastle.openpgp.bc.BcPGPObjectFactory; import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; -import org.bouncycastle.openpgp.operator.bc.BcPublicKeyDataDecryptorFactory; import org.bouncycastle.util.io.Streams; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.DocumentSignatureType; import org.pgpainless.algorithm.HashAlgorithm; @@ -49,16 +47,17 @@ import org.pgpainless.encryption_signing.SigningOptions; import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnlockSecretKey; +import org.pgpainless.util.TestAllImplementations; public class OnePassSignatureBracketingTest { - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void onePassSignaturePacketsAndSignaturesAreBracketedTest(ImplementationFactory implementationFactory) throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void onePassSignaturePacketsAndSignaturesAreBracketedTest() + throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { - PGPSecretKeyRing key1 = PGPainless.generateKeyRing().modernKeyRing("Alice", null); - PGPSecretKeyRing key2 = PGPainless.generateKeyRing().modernKeyRing("Bob", null); + PGPSecretKeyRing key1 = PGPainless.generateKeyRing().modernKeyRing("Alice"); + PGPSecretKeyRing key2 = PGPainless.generateKeyRing().modernKeyRing("Bob"); PGPPublicKeyRing cert1 = PGPainless.extractCertificate(key1); ByteArrayOutputStream out = new ByteArrayOutputStream(); @@ -79,7 +78,7 @@ public class OnePassSignatureBracketingTest { ByteArrayInputStream ciphertextIn = new ByteArrayInputStream(out.toByteArray()); InputStream inputStream = PGPUtil.getDecoderStream(ciphertextIn); - PGPObjectFactory objectFactory = new BcPGPObjectFactory(inputStream); + PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(inputStream); PGPOnePassSignatureList onePassSignatures = null; PGPSignatureList signatures = null; @@ -87,7 +86,7 @@ public class OnePassSignatureBracketingTest { outerloop: while (true) { Object next = objectFactory.nextObject(); if (next == null) { - break outerloop; + break; } if (next instanceof PGPEncryptedDataList) { PGPEncryptedDataList encryptedDataList = (PGPEncryptedDataList) next; @@ -96,9 +95,9 @@ public class OnePassSignatureBracketingTest { PGPPublicKeyEncryptedData publicKeyEncryptedData = (PGPPublicKeyEncryptedData) encryptedData; PGPSecretKey secretKey = key1.getSecretKey(publicKeyEncryptedData.getKeyID()); PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKey, SecretKeyRingProtector.unprotectedKeys()); - PublicKeyDataDecryptorFactory decryptorFactory = new BcPublicKeyDataDecryptorFactory(privateKey); + PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance().getPublicKeyDataDecryptorFactory(privateKey); InputStream decryptionStream = publicKeyEncryptedData.getDataStream(decryptorFactory); - objectFactory = new BcPGPObjectFactory(decryptionStream); + objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(decryptionStream); continue outerloop; } } @@ -108,7 +107,7 @@ public class OnePassSignatureBracketingTest { } else if (next instanceof PGPCompressedData) { PGPCompressedData compressed = (PGPCompressedData) next; InputStream decompressor = compressed.getDataStream(); - objectFactory = new PGPObjectFactory(decompressor, ImplementationFactory.getInstance().getKeyFingerprintCalculator()); + objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(decompressor); continue outerloop; } else if (next instanceof PGPLiteralData) { continue outerloop; @@ -145,7 +144,7 @@ public class OnePassSignatureBracketingTest { // 4 is hash algo assertEquals(HashAlgorithm.SHA512.getAlgorithmId(), encoded[4]); // 5 is public key algo - assertEquals(PublicKeyAlgorithm.EDDSA.getAlgorithmId(), encoded[5]); + assertEquals(PublicKeyAlgorithm.EDDSA_LEGACY.getAlgorithmId(), encoded[5]); // [6,7,8,9,10,11,12,13] are key-id boolean last = i == signatures.size() - 1; diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureOverUserAttributesTest.java b/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureOverUserAttributesTest.java index c8e8c821..16b241b4 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureOverUserAttributesTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureOverUserAttributesTest.java @@ -28,6 +28,7 @@ import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.TestKeys; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnlockSecretKey; +import org.pgpainless.signature.consumer.SignatureVerifier; public class SignatureOverUserAttributesTest { diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureStructureTest.java b/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureStructureTest.java index 14d57567..0d957309 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureStructureTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureStructureTest.java @@ -5,10 +5,12 @@ package org.pgpainless.signature; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import java.io.IOException; import java.util.List; +import org.bouncycastle.bcpg.sig.IssuerKeyID; import org.bouncycastle.bcpg.sig.NotationData; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPSignature; @@ -64,7 +66,7 @@ public class SignatureStructureTest { @Test public void testGetHashAlgorithm() { - assertEquals(HashAlgorithm.SHA256, HashAlgorithm.fromId(signature.getHashAlgorithm())); + assertEquals(HashAlgorithm.SHA256, HashAlgorithm.requireFromId(signature.getHashAlgorithm())); } @Test @@ -75,12 +77,14 @@ public class SignatureStructureTest { @Test public void testGetIssuerFingerprint() { assertEquals(new OpenPgpV4Fingerprint("D1A66E1A23B182C9980F788CFBFCC82A015E7330"), - SignatureSubpacketsUtil.getIssuerFingerprintAsOpenPgpV4Fingerprint(signature)); + SignatureSubpacketsUtil.getIssuerFingerprintAsOpenPgpFingerprint(signature)); } @Test public void testGetIssuer() { - assertEquals(KeyIdUtil.fromLongKeyId("FBFCC82A015E7330"), SignatureSubpacketsUtil.getIssuerKeyId(signature).getKeyID()); + IssuerKeyID issuerKeyID = SignatureSubpacketsUtil.getIssuerKeyId(signature); + assertNotNull(issuerKeyID); + assertEquals(KeyIdUtil.fromLongKeyId("FBFCC82A015E7330"), issuerKeyID.getKeyID()); } @Test diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureSubpacketsUtilTest.java b/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureSubpacketsUtilTest.java index 588a7191..1caeb9e9 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureSubpacketsUtilTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureSubpacketsUtilTest.java @@ -46,6 +46,7 @@ import org.pgpainless.key.TestKeys; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnlockSecretKey; import org.pgpainless.policy.Policy; +import org.pgpainless.signature.consumer.SignaturePicker; import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil; public class SignatureSubpacketsUtilTest { @@ -53,13 +54,14 @@ public class SignatureSubpacketsUtilTest { @Test public void testGetKeyExpirationTimeAsDate() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() - .modernKeyRing("Expire", null); + .modernKeyRing("Expire"); Date expiration = Date.from(new Date().toInstant().plus(365, ChronoUnit.DAYS)); secretKeys = PGPainless.modifyKeyRing(secretKeys) .setExpirationDate(expiration, SecretKeyRingProtector.unprotectedKeys()) .done(); - PGPSignature expirationSig = SignaturePicker.pickCurrentUserIdCertificationSignature(secretKeys, "Expire", Policy.getInstance(), new Date()); + PGPSignature expirationSig = SignaturePicker.pickCurrentUserIdCertificationSignature( + secretKeys, "Expire", Policy.getInstance(), new Date()); PGPPublicKey notTheRightKey = PGPainless.inspectKeyRing(secretKeys).getSigningSubkeys().get(0); assertThrows(IllegalArgumentException.class, () -> @@ -138,15 +140,16 @@ public class SignatureSubpacketsUtilTest { PGPSignatureGenerator generator = getSignatureGenerator(certKey, SignatureType.CASUAL_CERTIFICATION); PGPSignatureSubpacketGenerator hashed = new PGPSignatureSubpacketGenerator(); - hashed.setFeature(true, Feature.toBitmask(Feature.MODIFICATION_DETECTION, Feature.AEAD_ENCRYPTED_DATA)); + hashed.setFeature(true, Feature.toBitmask(Feature.MODIFICATION_DETECTION, Feature.LIBREPGP_OCB_ENCRYPTED_DATA)); generator.setHashedSubpackets(hashed.generate()); PGPSignature signature = generator.generateCertification(secretKeys.getPublicKey()); Set featureSet = SignatureSubpacketsUtil.parseFeatures(signature); + assertNotNull(featureSet); assertEquals(2, featureSet.size()); assertTrue(featureSet.contains(Feature.MODIFICATION_DETECTION)); - assertTrue(featureSet.contains(Feature.AEAD_ENCRYPTED_DATA)); - assertFalse(featureSet.contains(Feature.VERSION_5_PUBLIC_KEY)); + assertTrue(featureSet.contains(Feature.LIBREPGP_OCB_ENCRYPTED_DATA)); + assertFalse(featureSet.contains(Feature.LIBREPGP_VERSION_5_PUBLIC_KEY)); } @Test @@ -214,6 +217,7 @@ public class SignatureSubpacketsUtilTest { PGPSignature signature = generator.generateCertification(secretKeys.getPublicKey()); RevocationKey revocationKey = SignatureSubpacketsUtil.getRevocationKey(signature); + assertNotNull(revocationKey); assertArrayEquals(secretKeys.getPublicKey().getFingerprint(), revocationKey.getFingerprint()); assertEquals(secretKeys.getPublicKey().getAlgorithm(), revocationKey.getAlgorithm()); } @@ -275,6 +279,7 @@ public class SignatureSubpacketsUtilTest { PGPSignature signature = generator.generateCertification(secretKeys.getPublicKey()); TrustSignature trustSignature = SignatureSubpacketsUtil.getTrustSignature(signature); + assertNotNull(trustSignature); assertEquals(10, trustSignature.getDepth()); assertEquals(3, trustSignature.getTrustAmount()); } diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureUtilsTest.java b/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureUtilsTest.java index 296f20a8..f57ed2c4 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureUtilsTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureUtilsTest.java @@ -10,14 +10,16 @@ import java.io.IOException; import java.util.List; import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSignature; import org.junit.jupiter.api.Test; -import org.pgpainless.key.util.KeyIdUtil; +import org.pgpainless.PGPainless; public class SignatureUtilsTest { @Test - public void readSignaturesFromCompressedData() throws PGPException, IOException { + public void readSignaturesFromCompressedDataDoesNotAttemptDecompression() throws PGPException, IOException { String compressed = "-----BEGIN PGP MESSAGE-----\n" + "Version: PGPainless\n" + "\n" + @@ -29,9 +31,7 @@ public class SignatureUtilsTest { "=yZ18\n" + "-----END PGP MESSAGE-----"; List signatures = SignatureUtils.readSignatures(compressed); - assertEquals(2, signatures.size()); - assertEquals(KeyIdUtil.fromLongKeyId("5736E6931ACF370C"), signatures.get(0).getKeyID()); - assertEquals(KeyIdUtil.fromLongKeyId("F49AAA6B067BAB28"), signatures.get(1).getKeyID()); + assertEquals(0, signatures.size()); } @Test @@ -86,4 +86,57 @@ public class SignatureUtilsTest { List signatures = SignatureUtils.readSignatures(sigs); assertEquals(1, signatures.size()); // first sig gets skipped } + + @Test + public void testGetSignaturesOverUserIdBy() throws IOException { + String alice = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 9CA2 8D6D DBA6 BCF1 23A4 2775 0EB5 08CD 1714 B46A\n" + + "Comment: Alice \n" + + "Comment: 1 further identity\n" + + "\n" + + "mDMEYoPLwhYJKwYBBAHaRw8BAQdAnuduN87Gu2qvsfdRxLP83strq+doPNP8Hx2J\n" + + "esvaN0+0GUFsaWNlIDxhbGljZUBleG1hcGxlLmNvbT6IjwQTFgoAQQUCYoPLwgkQ\n" + + "DrUIzRcUtGoWIQScoo1t26a88SOkJ3UOtQjNFxS0agKeAQKbAQUWAgMBAAQLCQgH\n" + + "BRUKCQgLApkBAABRRwD+II62grSOGKDyBYMLTfCNQejcazQYWoSVyJiD308CRxgA\n" + + "/2H6kTXaV+Lk2+te/yZ3aeAd1wFBDe2HRelrMy4074gMiHUEEBYKACcFAmKDy8IJ\n" + + "EE3a6g4UHIzBFiEE0VukWebIQb/PImHfTdrqDhQcjMEAAOjCAQCcCQySwr/8VgW8\n" + + "Ww+pKM21gWWSGMazMqAcDwqnCrebtAEAiU2PtfWGFZc6VVdsMI1GOcRp++fz+AJ5\n" + + "fqzWZ+QBBgK0LUFsaWNlIEV4YW1wbGUgPGFsaWNlQGV4YW1wbGUuY29tPiBbZnJv\n" + + "bSB3b3JrXYh1BBAWCgAnBQJig8vCCRC2GO3iDTVMtxYhBKl1XHhzEUcOxqNPwLYY\n" + + "7eINNUy3AADMFQD+Pcfk5nT7P4KDBxYiLs8Jct3dWLoOMR7dY9jn43d4Q6IBANWy\n" + + "DqBF1IsqTeqRaKUVKw8sWrEIZcgFt7SpgcsLTHMOuDgEYoPLwhIKKwYBBAGXVQEF\n" + + "AQEHQKY2huLPeGlqnLi4ITEgbtYp/C4ofZjmh6/rKUirtopIAwEIB4h1BBgWCgAd\n" + + "BQJig8vCAp4BApsMBRYCAwEABAsJCAcFFQoJCAsACgkQDrUIzRcUtGp9qQD+KuK+\n" + + "lWnlioN8gEyh1Rl2b4ABH6hOBdfW6zjUggnvVHwBAN6r6MJdu47c9xsLKypzyhwB\n" + + "0RbnyH5NMS6jwsK5zmoOuDMEYoPLwhYJKwYBBAHaRw8BAQdAxst2EY4/drt/MeTU\n" + + "RkzQdB8AO1Wc2gnlXavk2a+0DpyI1QQYFgoAfQUCYoPLwgKeAQKbAgUWAgMBAAQL\n" + + "CQgHBRUKCQgLXyAEGRYKAAYFAmKDy8IACgkQchAyuqB7Hn2yOAD/cPA01NO5YJPg\n" + + "KUuSDLnk872y+e419bvFizrM4LKYbeoA/0aw12mcpi1smQJ3mm9T/oGidatBQJ74\n" + + "JIPqTtwHSTIHAAoJEA61CM0XFLRqzj4A+QGjS6ay2AioirHJ9SCA8Eq6L2f/N3RB\n" + + "YBOlV32f3zxyAP9fwXlz0hRbBDnnie2O5eXT9ZurnAKGXPwCtlsqrmeTBg==\n" + + "=uC3F\n" + + "-----END PGP PUBLIC KEY BLOCK-----\n"; + + String aliceId = "Alice "; + // TODO: Fix wrong user-id format of pet name + String charliesPetNameForAlice = "Alice Example [from work]"; + + long aliceKeyId = 1059762964264170602L; + long bobKeyId = 5610053632031231169L; + long charlieKeyId = -5325245004225622857L; + + PGPPublicKeyRing aliceCert = PGPainless.readKeyRing().publicKeyRing(alice); + PGPPublicKey aliceKey = aliceCert.getPublicKey(); + + // alice self-signed her user-id + assertEquals(1, SignatureUtils.getSignaturesOverUserIdBy(aliceKey, aliceId, aliceKeyId).size()); + // Bob signed alices user-id + assertEquals(1, SignatureUtils.getSignaturesOverUserIdBy(aliceKey, aliceId, bobKeyId).size()); + // charlie gave alice a pet name + assertEquals(1, SignatureUtils.getSignaturesOverUserIdBy(aliceKey, charliesPetNameForAlice, charlieKeyId).size()); + + // Alice did not certify the petname charlie gave her + assertEquals(0, SignatureUtils.getSignaturesOverUserIdBy(aliceKey, charliesPetNameForAlice, aliceKeyId).size()); + } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureWasPossiblyMadeByKeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureWasPossiblyMadeByKeyTest.java index f01331b3..c7424af1 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureWasPossiblyMadeByKeyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureWasPossiblyMadeByKeyTest.java @@ -17,6 +17,7 @@ import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.exception.SignatureValidationException; import org.pgpainless.key.OpenPgpV4Fingerprint; +import org.pgpainless.signature.consumer.SignatureValidator; public class SignatureWasPossiblyMadeByKeyTest { diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/builder/SubkeyAndPrimaryKeyBindingSignatureTest.java b/pgpainless-core/src/test/java/org/pgpainless/signature/builder/SubkeyAndPrimaryKeyBindingSignatureTest.java new file mode 100644 index 00000000..e44af522 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/builder/SubkeyAndPrimaryKeyBindingSignatureTest.java @@ -0,0 +1,64 @@ +// SPDX-FileCopyrightText: 2021 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.builder; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.EncryptionPurpose; +import org.pgpainless.algorithm.HashAlgorithm; +import org.pgpainless.algorithm.KeyFlag; +import org.pgpainless.key.TestKeys; +import org.pgpainless.key.info.KeyRingInfo; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.key.util.KeyRingUtils; +import org.pgpainless.signature.subpackets.SelfSignatureSubpackets; + +public class SubkeyAndPrimaryKeyBindingSignatureTest { + + @Test + public void testRebindSubkey() throws PGPException, IOException { + PGPSecretKeyRing secretKeys = TestKeys.getEmilSecretKeyRing(); + KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); + + PGPSecretKey primaryKey = secretKeys.getSecretKey(); + PGPPublicKey encryptionSubkey = info.getEncryptionSubkeys(EncryptionPurpose.ANY).get(0); + assertNotNull(encryptionSubkey); + + Set hashAlgorithmSet = info.getPreferredHashAlgorithms(encryptionSubkey.getKeyID()); + assertEquals( + new HashSet<>(Arrays.asList( + HashAlgorithm.SHA512, HashAlgorithm.SHA384, HashAlgorithm.SHA256, HashAlgorithm.SHA224)), + hashAlgorithmSet); + + SubkeyBindingSignatureBuilder sbb = new SubkeyBindingSignatureBuilder(primaryKey, SecretKeyRingProtector.unprotectedKeys()); + sbb.applyCallback(new SelfSignatureSubpackets.Callback() { + @Override + public void modifyHashedSubpackets(SelfSignatureSubpackets hashedSubpackets) { + hashedSubpackets.setKeyFlags(KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE); + hashedSubpackets.setPreferredHashAlgorithms(HashAlgorithm.SHA512); + } + }); + + PGPSignature binding = sbb.build(encryptionSubkey); + secretKeys = KeyRingUtils.injectCertification(secretKeys, encryptionSubkey, binding); + + info = PGPainless.inspectKeyRing(secretKeys); + assertEquals(Collections.singleton(HashAlgorithm.SHA512), info.getPreferredHashAlgorithms(encryptionSubkey.getKeyID())); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/builder/ThirdPartyCertificationSignatureBuilderTest.java b/pgpainless-core/src/test/java/org/pgpainless/signature/builder/ThirdPartyCertificationSignatureBuilderTest.java new file mode 100644 index 00000000..2b0f4d35 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/builder/ThirdPartyCertificationSignatureBuilderTest.java @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: 2021 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.builder; + +import org.bouncycastle.bcpg.sig.Exportable; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.SignatureType; +import org.pgpainless.implementation.ImplementationFactory; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.signature.subpackets.CertificationSubpackets; +import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil; + +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class ThirdPartyCertificationSignatureBuilderTest { + + @Test + public void testInvalidSignatureTypeThrows() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() + .modernKeyRing("Alice"); + assertThrows(IllegalArgumentException.class, () -> + new ThirdPartyCertificationSignatureBuilder( + SignatureType.BINARY_DOCUMENT, // invalid type + secretKeys.getSecretKey(), + SecretKeyRingProtector.unprotectedKeys())); + } + + @Test + public void testUserIdCertification() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() + .modernKeyRing("Alice"); + + PGPPublicKeyRing bobsPublicKeys = PGPainless.extractCertificate( + PGPainless.generateKeyRing().modernKeyRing("Bob")); + + ThirdPartyCertificationSignatureBuilder signatureBuilder = new ThirdPartyCertificationSignatureBuilder( + secretKeys.getSecretKey(), + SecretKeyRingProtector.unprotectedKeys()); + + signatureBuilder.applyCallback(new CertificationSubpackets.Callback() { + @Override + public void modifyHashedSubpackets(CertificationSubpackets hashedSubpackets) { + hashedSubpackets.setExportable(true, false); + } + }); + + PGPSignature certification = signatureBuilder.build(bobsPublicKeys, "Bob"); + assertEquals(SignatureType.GENERIC_CERTIFICATION, SignatureType.valueOf(certification.getSignatureType())); + assertEquals(secretKeys.getPublicKey().getKeyID(), certification.getKeyID()); + assertArrayEquals(secretKeys.getPublicKey().getFingerprint(), certification.getHashedSubPackets().getIssuerFingerprint().getFingerprint()); + Exportable exportable = SignatureSubpacketsUtil.getExportableCertification(certification); + assertNotNull(exportable); + assertFalse(exportable.isExportable()); + + // test sig correctness + certification.init(ImplementationFactory.getInstance().getPgpContentVerifierBuilderProvider(), secretKeys.getPublicKey()); + assertTrue(certification.verifyCertification("Bob", bobsPublicKeys.getPublicKey())); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/builder/ThirdPartyDirectKeySignatureBuilderTest.java b/pgpainless-core/src/test/java/org/pgpainless/signature/builder/ThirdPartyDirectKeySignatureBuilderTest.java new file mode 100644 index 00000000..56605f83 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/builder/ThirdPartyDirectKeySignatureBuilderTest.java @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: 2021 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.builder; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.util.Collections; +import java.util.Date; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.CompressionAlgorithm; +import org.pgpainless.algorithm.Feature; +import org.pgpainless.algorithm.HashAlgorithm; +import org.pgpainless.algorithm.KeyFlag; +import org.pgpainless.algorithm.SignatureType; +import org.pgpainless.algorithm.SymmetricKeyAlgorithm; +import org.pgpainless.key.info.KeyRingInfo; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.key.util.KeyRingUtils; +import org.pgpainless.signature.subpackets.SelfSignatureSubpackets; +import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil; + +public class ThirdPartyDirectKeySignatureBuilderTest { + + @Test + public void testDirectKeySignatureBuilding() throws PGPException { + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() + .modernKeyRing("Alice"); + + DirectKeySelfSignatureBuilder dsb = new DirectKeySelfSignatureBuilder( + secretKeys.getSecretKey(), + SecretKeyRingProtector.unprotectedKeys()); + + Date now = new Date(); + Date t1 = new Date(now.getTime() + 1000 * 60 * 60); + dsb.applyCallback(new SelfSignatureSubpackets.Callback() { + @Override + public void modifyHashedSubpackets(SelfSignatureSubpackets hashedSubpackets) { + hashedSubpackets.setSignatureCreationTime(t1); + hashedSubpackets.setKeyFlags(KeyFlag.CERTIFY_OTHER); + hashedSubpackets.setPreferredHashAlgorithms(HashAlgorithm.SHA512); + hashedSubpackets.setPreferredCompressionAlgorithms(CompressionAlgorithm.ZIP); + hashedSubpackets.setPreferredSymmetricKeyAlgorithms(SymmetricKeyAlgorithm.AES_256); + hashedSubpackets.setFeatures(Feature.MODIFICATION_DETECTION); + } + }); + + PGPSignature directKeySig = dsb.build(); + assertNotNull(directKeySig); + secretKeys = KeyRingUtils.injectCertification(secretKeys, secretKeys.getPublicKey(), directKeySig); + + KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys, t1); + PGPSignature signature = info.getLatestDirectKeySelfSignature(); + + assertNotNull(signature); + assertEquals(directKeySig, signature); + + assertEquals(SignatureType.DIRECT_KEY, SignatureType.valueOf(signature.getSignatureType())); + assertEquals(Collections.singletonList(KeyFlag.CERTIFY_OTHER), SignatureSubpacketsUtil.parseKeyFlags(signature)); + assertEquals(Collections.singleton(HashAlgorithm.SHA512), SignatureSubpacketsUtil.parsePreferredHashAlgorithms(signature)); + assertEquals(Collections.singleton(CompressionAlgorithm.ZIP), SignatureSubpacketsUtil.parsePreferredCompressionAlgorithms(signature)); + assertEquals(Collections.singleton(SymmetricKeyAlgorithm.AES_256), SignatureSubpacketsUtil.parsePreferredSymmetricKeyAlgorithms(signature)); + assertEquals(secretKeys.getPublicKey().getKeyID(), signature.getKeyID()); + assertArrayEquals(secretKeys.getPublicKey().getFingerprint(), signature.getHashedSubPackets().getIssuerFingerprint().getFingerprint()); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/builder/UniversalSignatureBuilderTest.java b/pgpainless-core/src/test/java/org/pgpainless/signature/builder/UniversalSignatureBuilderTest.java new file mode 100644 index 00000000..37bc6fd3 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/builder/UniversalSignatureBuilderTest.java @@ -0,0 +1,90 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.builder; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +import java.io.IOException; + +import org.bouncycastle.bcpg.sig.PrimaryUserID; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.PGPSignatureGenerator; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.HashAlgorithm; +import org.pgpainless.algorithm.KeyFlag; +import org.pgpainless.algorithm.SignatureType; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.signature.subpackets.SignatureSubpackets; + +public class UniversalSignatureBuilderTest { + + private static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 9611 510F 313E DBC2 BBBC DC24 3BAD F1F8 3E70 DC34\n" + + "Comment: Signora Universa \n" + + "\n" + + "lFgEY4DKKRYJKwYBBAHaRw8BAQdA65vJxvvLASI/gczDP8ZKH4C+16MLU7F5iP91\n" + + "8WWUqM0AAQCRSTHLLQWT9tuNRgkG3xaIiBGkEGD7Ou/R3oga6tc1MA8UtClTaWdu\n" + + "b3JhIFVuaXZlcnNhIDxzaWdub3JhQHBncGFpbmxlc3Mub3JnPoiPBBMWCgBBBQJj\n" + + "gMopCRA7rfH4PnDcNBYhBJYRUQ8xPtvCu7zcJDut8fg+cNw0Ap4BApsBBRYCAwEA\n" + + "BAsJCAcFFQoJCAsCmQEAAOgMAPwIOXWt3EBBusK5Ps3m7p/5HsecZv3IXtscEQBx\n" + + "vKlULwD/YuLP1XJSqcE2cQJRNt6OLi9Nt02MKBYkhWrRCYZAcQicXQRjgMopEgor\n" + + "BgEEAZdVAQUBAQdAWTstuhvHwmSXaQ4Vh8yxl0DZcvjrWkZI+n9/uFBxEmoDAQgH\n" + + "AAD/eRt6kgOMzWsTuM00am4UhSygxmDt7h6JkBTnpyyhK0gPiYh1BBgWCgAdBQJj\n" + + "gMopAp4BApsMBRYCAwEABAsJCAcFFQoJCAsACgkQO63x+D5w3DRnZAEA6GlS9Tw8\n" + + "9SJlUvh5aciYSlQUplnEdng+Pvzbj74zcXIA/2OkyMN428ddNhkHWWkZCMOxApum\n" + + "/zNDSYMwvByQ2KcFnFgEY4DKKRYJKwYBBAHaRw8BAQdAfhPrtVuG3g/zXF51VrPv\n" + + "kpQQk9aqjrkBMI0qlztBpu0AAP9Mw7NCsAVwg9CgmSzG2ATIDp3yf/4BGVYDs7qu\n" + + "+sbn7xKIiNUEGBYKAH0FAmOAyikCngECmwIFFgIDAQAECwkIBwUVCgkIC18gBBkW\n" + + "CgAGBQJjgMopAAoJENmzwZA/hq5ZCqIBAMYeOnASBd+WWta7Teh3g7Bl7sFY42Qy\n" + + "0OnaSGk/pLm9AP4yC62Xpb9DhWeiQIOY7k5n4lhNn173IfzDK6KXzBKkBgAKCRA7\n" + + "rfH4PnDcNMInAP4oanG9tbuczBNLN3JY4Hg4AaB+w5kfdOJxKwnAw7U0cgEAtasg\n" + + "67qSjHvsEvjNKeXzUm+db7NWP3fpIHxAmjWVjwM=\n" + + "=Dqbd\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + + private PGPSecretKeyRing secretKeys; + private final SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); + + @BeforeEach + public void parseKey() throws IOException { + secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); + } + + @Test + public void createPetNameSignature() throws PGPException { + PGPSecretKey signingKey = secretKeys.getSecretKey(); + PGPSignature archetype = signingKey.getPublicKey().getSignatures().next(); + UniversalSignatureBuilder builder = new UniversalSignatureBuilder( + signingKey, protector, archetype); + + builder.applyCallback(new SignatureSubpackets.Callback() { + @Override + public void modifyHashedSubpackets(SignatureSubpackets hashedSubpackets) { + hashedSubpackets.setExportable(true, false); + hashedSubpackets.setPrimaryUserId(new PrimaryUserID(false, false)); + } + }); + + PGPSignatureGenerator generator = builder.getSignatureGenerator(); + + String petName = "mykey"; + PGPSignature petNameSig = generator.generateCertification(petName, secretKeys.getPublicKey()); + + assertEquals(SignatureType.POSITIVE_CERTIFICATION.getCode(), petNameSig.getSignatureType()); + assertEquals(4, petNameSig.getVersion()); + assertEquals(signingKey.getKeyID(), petNameSig.getKeyID()); + assertEquals(HashAlgorithm.SHA512.getAlgorithmId(), petNameSig.getHashAlgorithm()); + assertEquals(KeyFlag.toBitmask(KeyFlag.CERTIFY_OTHER), petNameSig.getHashedSubPackets().getKeyFlags()); + assertFalse(petNameSig.getHashedSubPackets().isExportable()); + assertFalse(petNameSig.getHashedSubPackets().isPrimaryUserID()); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/subpackets/SignatureSubpacketsTest.java b/pgpainless-core/src/test/java/org/pgpainless/signature/subpackets/SignatureSubpacketsTest.java new file mode 100644 index 00000000..14eed4de --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/subpackets/SignatureSubpacketsTest.java @@ -0,0 +1,536 @@ +// SPDX-FileCopyrightText: 2021 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.subpackets; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.util.Date; +import java.util.Iterator; +import java.util.Random; + +import org.bouncycastle.bcpg.SignatureSubpacket; +import org.bouncycastle.bcpg.SignatureSubpacketTags; +import org.bouncycastle.bcpg.sig.Exportable; +import org.bouncycastle.bcpg.sig.Features; +import org.bouncycastle.bcpg.sig.IssuerFingerprint; +import org.bouncycastle.bcpg.sig.NotationData; +import org.bouncycastle.bcpg.sig.PreferredAlgorithms; +import org.bouncycastle.bcpg.sig.Revocable; +import org.bouncycastle.bcpg.sig.RevocationKey; +import org.bouncycastle.bcpg.sig.RevocationReason; +import org.bouncycastle.bcpg.sig.SignatureTarget; +import org.bouncycastle.bcpg.sig.TrustSignature; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPrivateKey; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.PGPSignatureGenerator; +import org.bouncycastle.openpgp.PGPSignatureSubpacketGenerator; +import org.bouncycastle.openpgp.PGPSignatureSubpacketVector; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.pgpainless.algorithm.CompressionAlgorithm; +import org.pgpainless.algorithm.Feature; +import org.pgpainless.algorithm.HashAlgorithm; +import org.pgpainless.algorithm.KeyFlag; +import org.pgpainless.algorithm.PublicKeyAlgorithm; +import org.pgpainless.algorithm.SignatureType; +import org.pgpainless.algorithm.SymmetricKeyAlgorithm; +import org.pgpainless.implementation.ImplementationFactory; +import org.pgpainless.key.OpenPgpFingerprint; +import org.pgpainless.key.TestKeys; +import org.pgpainless.key.protection.UnlockSecretKey; +import org.pgpainless.key.util.RevocationAttributes; +import org.pgpainless.util.DateUtil; +import org.pgpainless.util.Passphrase; + +public class SignatureSubpacketsTest { + + private static PGPPublicKeyRing keys; + private static PGPPublicKey key; + + private SignatureSubpackets wrapper; + + @BeforeAll + public static void setup() throws IOException { + keys = TestKeys.getEmilPublicKeyRing(); + key = keys.getPublicKey(); + } + + @BeforeEach + public void createWrapper() { + wrapper = SignatureSubpackets.createHashedSubpackets(key); + } + + @Test + public void initialStateTest() { + wrapper = new SignatureSubpackets(); + PGPSignatureSubpacketVector vector = SignatureSubpacketsHelper.toVector(wrapper); + assertNull(vector.getSignatureCreationTime()); + } + + @Test + public void initialStateFromKeyTest() throws PGPException { + PGPSignatureSubpacketVector vector = SignatureSubpacketsHelper.toVector(wrapper); + + assertEquals(key.getKeyID(), vector.getIssuerKeyID()); + assertEquals(key.getVersion(), vector.getIssuerFingerprint().getKeyVersion()); + assertArrayEquals(key.getFingerprint(), vector.getIssuerFingerprint().getFingerprint()); + assertNull(vector.getSignatureCreationTime()); + + assertEquals(0, vector.getKeyFlags()); + assertEquals(0, vector.getSignatureExpirationTime()); + assertNull(vector.getSignerUserID()); + assertFalse(vector.isPrimaryUserID()); + assertEquals(0, vector.getKeyExpirationTime()); + assertNull(vector.getPreferredCompressionAlgorithms()); + assertNull(vector.getPreferredSymmetricAlgorithms()); + assertNull(vector.getPreferredHashAlgorithms()); + assertEquals(0, vector.getNotationDataOccurrences().length); + assertNull(vector.getIntendedRecipientFingerprint()); + assertNull(vector.getSubpacket(SignatureSubpacketTags.EXPORTABLE)); + assertNull(vector.getSubpacket(SignatureSubpacketTags.REVOCATION_KEY)); + assertNull(vector.getSubpacket(SignatureSubpacketTags.REVOCATION_REASON)); + assertNull(vector.getSignatureTarget()); + assertNull(vector.getFeatures()); + assertNull(vector.getSubpacket(SignatureSubpacketTags.TRUST_SIG)); + assertTrue(vector.getEmbeddedSignatures().isEmpty()); + } + + @Test + public void testNullKeyId() { + wrapper.setIssuerKeyId(null); + PGPSignatureSubpacketVector vector = SignatureSubpacketsHelper.toVector(wrapper); + + assertEquals(0, vector.getIssuerKeyID()); + } + + + @Test + public void testNullFingerprint() { + wrapper.setIssuerFingerprint((IssuerFingerprint) null); + PGPSignatureSubpacketVector vector = SignatureSubpacketsHelper.toVector(wrapper); + + assertNull(vector.getIssuerFingerprint()); + } + + @Test + public void testAddNotationData() { + wrapper.addNotationData(true, "critical@notation.data", "isCritical"); + wrapper.addNotationData(false, "noncrit@notation.data", "notCritical"); + PGPSignatureSubpacketVector vector = SignatureSubpacketsHelper.toVector(wrapper); + + NotationData[] notationData = vector.getNotationDataOccurrences(); + assertEquals(2, notationData.length); + NotationData first = notationData[0]; + assertTrue(first.isCritical()); + assertTrue(first.isHumanReadable()); + assertEquals("critical@notation.data", first.getNotationName()); + assertEquals("isCritical", first.getNotationValue()); + + NotationData second = notationData[1]; + assertFalse(second.isCritical()); + assertTrue(second.isHumanReadable()); + assertEquals("noncrit@notation.data", second.getNotationName()); + assertEquals("notCritical", second.getNotationValue()); + + wrapper.clearNotationData(); + vector = SignatureSubpacketsHelper.toVector(wrapper); + assertEquals(0, vector.getNotationDataOccurrences().length); + + } + + @Test + public void testIntendedRecipientFingerprints() { + wrapper.addIntendedRecipientFingerprint(key); + PGPSignatureSubpacketVector vector = SignatureSubpacketsHelper.toVector(wrapper); + + assertEquals(1, vector.getSubpackets(SignatureSubpacketTags.INTENDED_RECIPIENT_FINGERPRINT).length); + assertArrayEquals(key.getFingerprint(), vector.getIntendedRecipientFingerprint().getFingerprint()); + assertEquals(key.getVersion(), vector.getIntendedRecipientFingerprint().getKeyVersion()); + + wrapper.clearIntendedRecipientFingerprints(); + vector = SignatureSubpacketsHelper.toVector(wrapper); + assertEquals(0, vector.getSubpackets(SignatureSubpacketTags.INTENDED_RECIPIENT_FINGERPRINT).length); + } + + @Test + public void testAddRevocationKeys() { + Iterator keyIterator = keys.getPublicKeys(); + PGPPublicKey first = keyIterator.next(); + wrapper.addRevocationKey(first); + assertTrue(keyIterator.hasNext()); + PGPPublicKey second = keyIterator.next(); + wrapper.addRevocationKey(false, true, second); + PGPSignatureSubpacketVector vector = SignatureSubpacketsHelper.toVector(wrapper); + + SignatureSubpacket[] revKeys = vector.getSubpackets(SignatureSubpacketTags.REVOCATION_KEY); + assertEquals(2, revKeys.length); + RevocationKey r1 = (RevocationKey) revKeys[0]; + RevocationKey r2 = (RevocationKey) revKeys[1]; + + assertTrue(r1.isCritical()); + assertArrayEquals(first.getFingerprint(), r1.getFingerprint()); + assertEquals(first.getAlgorithm(), r1.getAlgorithm()); + assertEquals((byte) 0x80, r1.getSignatureClass()); + + assertFalse(r2.isCritical()); + assertArrayEquals(second.getFingerprint(), r2.getFingerprint()); + assertEquals(second.getAlgorithm(), r2.getAlgorithm()); + assertEquals((byte) (0x80 | 0x40), r2.getSignatureClass()); + + wrapper.clearRevocationKeys(); + vector = SignatureSubpacketsHelper.toVector(wrapper); + assertEquals(0, vector.getSubpackets(SignatureSubpacketTags.REVOCATION_KEY).length); + } + + @Test + public void testSetKeyFlags() { + wrapper.setKeyFlags(KeyFlag.SIGN_DATA, KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA); // duplicates are removed + PGPSignatureSubpacketVector vector = SignatureSubpacketsHelper.toVector(wrapper); + + assertEquals(KeyFlag.toBitmask(KeyFlag.SIGN_DATA, KeyFlag.CERTIFY_OTHER), vector.getKeyFlags()); + assertTrue(vector.getSubpacket(SignatureSubpacketTags.KEY_FLAGS).isCritical()); + } + + @Test + public void testSignatureExpirationTime() { + Date now = new Date(); + long secondsInAWeek = 60 * 60 * 24 * 7; + Date inAWeek = new Date(now.getTime() + 1000 * secondsInAWeek); + wrapper.setSignatureExpirationTime(now, inAWeek); + PGPSignatureSubpacketVector vector = SignatureSubpacketsHelper.toVector(wrapper); + + assertEquals(secondsInAWeek, vector.getSignatureExpirationTime()); + } + + @Test + public void testSignatureExpirationTimeCannotBeNegative() { + Date now = new Date(); + long secondsInAWeek = 60 * 60 * 24 * 7; + Date oneWeekEarlier = new Date(now.getTime() - 1000 * secondsInAWeek); + assertThrows(IllegalArgumentException.class, () -> wrapper.setSignatureExpirationTime(now, oneWeekEarlier)); + } + + @Test + public void testSignerUserId() { + String userId = "Alice "; + wrapper.setSignerUserId(userId); + PGPSignatureSubpacketVector vector = SignatureSubpacketsHelper.toVector(wrapper); + + assertEquals(userId, vector.getSignerUserID()); + } + + @Test + public void testSetPrimaryUserId() { + PGPSignatureSubpacketVector vector = SignatureSubpacketsHelper.toVector(wrapper); + assertFalse(vector.isPrimaryUserID()); + + wrapper.setPrimaryUserId(); + vector = SignatureSubpacketsHelper.toVector(wrapper); + assertTrue(vector.isPrimaryUserID()); + } + + @Test + public void testSetKeyExpiration() { + Date now = new Date(); + long secondsSinceKeyCreation = (now.getTime() - key.getCreationTime().getTime()) / 1000; + wrapper.setKeyExpirationTime(key, now); + PGPSignatureSubpacketVector vector = SignatureSubpacketsHelper.toVector(wrapper); + + assertEquals(secondsSinceKeyCreation, vector.getKeyExpirationTime()); + } + + @Test + public void testSetKeyExpirationCannotBeNegative() { + Date beforeKeyCreation = new Date(key.getCreationTime().getTime() - 10000); + assertThrows(IllegalArgumentException.class, () -> wrapper.setKeyExpirationTime(key, beforeKeyCreation)); + } + + @Test + public void testSetPreferredCompressionAlgorithms() { + wrapper.setPreferredCompressionAlgorithms(CompressionAlgorithm.BZIP2, CompressionAlgorithm.ZIP, CompressionAlgorithm.BZIP2); // duplicates get removed + PGPSignatureSubpacketVector vector = SignatureSubpacketsHelper.toVector(wrapper); + + int[] ids = vector.getPreferredCompressionAlgorithms(); + assertEquals(2, ids.length); + assertEquals(CompressionAlgorithm.BZIP2.getAlgorithmId(), ids[0]); + assertEquals(CompressionAlgorithm.ZIP.getAlgorithmId(), ids[1]); + + wrapper.setPreferredCompressionAlgorithms(); // empty + vector = SignatureSubpacketsHelper.toVector(wrapper); + assertEquals(0, vector.getPreferredCompressionAlgorithms().length); + + wrapper.setPreferredCompressionAlgorithms((PreferredAlgorithms) null); + vector = SignatureSubpacketsHelper.toVector(wrapper); + assertNull(vector.getPreferredCompressionAlgorithms()); + + assertThrows(IllegalArgumentException.class, () -> wrapper.setPreferredCompressionAlgorithms( + new PreferredAlgorithms(SignatureSubpacketTags.PREFERRED_SYM_ALGS, true, new int[0]))); + } + + @Test + public void testSetPreferredSymmetricKeyAlgorithms() { + wrapper.setPreferredSymmetricKeyAlgorithms(SymmetricKeyAlgorithm.AES_192, SymmetricKeyAlgorithm.AES_128, SymmetricKeyAlgorithm.AES_128); // duplicates get removed + PGPSignatureSubpacketVector vector = SignatureSubpacketsHelper.toVector(wrapper); + + int[] ids = vector.getPreferredSymmetricAlgorithms(); + assertEquals(2, ids.length); + assertEquals(SymmetricKeyAlgorithm.AES_192.getAlgorithmId(), ids[0]); + assertEquals(SymmetricKeyAlgorithm.AES_128.getAlgorithmId(), ids[1]); + + wrapper.setPreferredSymmetricKeyAlgorithms(); // empty + vector = SignatureSubpacketsHelper.toVector(wrapper); + assertEquals(0, vector.getPreferredSymmetricAlgorithms().length); + + wrapper.setPreferredSymmetricKeyAlgorithms((PreferredAlgorithms) null); + vector = SignatureSubpacketsHelper.toVector(wrapper); + assertNull(vector.getPreferredCompressionAlgorithms()); + + assertThrows(IllegalArgumentException.class, () -> wrapper.setPreferredSymmetricKeyAlgorithms( + new PreferredAlgorithms(SignatureSubpacketTags.PREFERRED_HASH_ALGS, true, new int[0]))); + } + + @Test + public void testSetPreferredHashAlgorithms() { + wrapper.setPreferredHashAlgorithms(HashAlgorithm.SHA512, HashAlgorithm.SHA384, HashAlgorithm.SHA512); // duplicates get removed + PGPSignatureSubpacketVector vector = SignatureSubpacketsHelper.toVector(wrapper); + + int[] ids = vector.getPreferredHashAlgorithms(); + assertEquals(2, ids.length); + assertEquals(HashAlgorithm.SHA512.getAlgorithmId(), ids[0]); + assertEquals(HashAlgorithm.SHA384.getAlgorithmId(), ids[1]); + + wrapper.setPreferredHashAlgorithms(); // empty + vector = SignatureSubpacketsHelper.toVector(wrapper); + assertEquals(0, vector.getPreferredHashAlgorithms().length); + + wrapper.setPreferredHashAlgorithms((PreferredAlgorithms) null); + vector = SignatureSubpacketsHelper.toVector(wrapper); + assertNull(vector.getPreferredHashAlgorithms()); + + assertThrows(IllegalArgumentException.class, () -> wrapper.setPreferredHashAlgorithms( + new PreferredAlgorithms(SignatureSubpacketTags.PREFERRED_COMP_ALGS, true, new int[0]))); + } + + @Test + public void testSetExportable() { + wrapper.setExportable(true, false); + PGPSignatureSubpacketVector vector = SignatureSubpacketsHelper.toVector(wrapper); + + Exportable exportable = (Exportable) vector.getSubpacket(SignatureSubpacketTags.EXPORTABLE); + assertTrue(exportable.isCritical()); + assertFalse(exportable.isExportable()); + + wrapper.setExportable(false, true); + vector = SignatureSubpacketsHelper.toVector(wrapper); + + exportable = (Exportable) vector.getSubpacket(SignatureSubpacketTags.EXPORTABLE); + assertFalse(exportable.isCritical()); + assertTrue(exportable.isExportable()); + } + + @Test + public void testSetRevocable() { + wrapper.setRevocable(true, true); + PGPSignatureSubpacketVector vector = SignatureSubpacketsHelper.toVector(wrapper); + + Revocable revocable = (Revocable) vector.getSubpacket(SignatureSubpacketTags.REVOCABLE); + assertTrue(revocable.isCritical()); + assertTrue(revocable.isRevocable()); + + wrapper.setRevocable(false, false); + vector = SignatureSubpacketsHelper.toVector(wrapper); + + revocable = (Revocable) vector.getSubpacket(SignatureSubpacketTags.REVOCABLE); + assertFalse(revocable.isCritical()); + assertFalse(revocable.isRevocable()); + } + + @Test + public void testSetRevocationReason() { + wrapper.setRevocationReason(RevocationAttributes.createKeyRevocation() + .withReason(RevocationAttributes.Reason.KEY_RETIRED).withDescription("The key is too weak.")); + PGPSignatureSubpacketVector vector = SignatureSubpacketsHelper.toVector(wrapper); + + assertEquals(1, vector.getSubpackets(SignatureSubpacketTags.REVOCATION_REASON).length); + RevocationReason reason = (RevocationReason) vector.getSubpacket(SignatureSubpacketTags.REVOCATION_REASON); + assertEquals(RevocationAttributes.Reason.KEY_RETIRED.code(), reason.getRevocationReason()); + assertEquals("The key is too weak.", reason.getRevocationDescription()); + } + + @Test + public void testSetSignatureTarget() { + byte[] hash = new byte[20]; + new Random().nextBytes(hash); + wrapper.setSignatureTarget(PublicKeyAlgorithm.requireFromId(key.getAlgorithm()), HashAlgorithm.SHA512, hash); + PGPSignatureSubpacketVector vector = SignatureSubpacketsHelper.toVector(wrapper); + + SignatureTarget target = vector.getSignatureTarget(); + assertNotNull(target); + assertEquals(key.getAlgorithm(), target.getPublicKeyAlgorithm()); + assertEquals(HashAlgorithm.SHA512.getAlgorithmId(), target.getHashAlgorithm()); + assertArrayEquals(hash, target.getHashData()); + } + + @Test + public void testSetFeatures() { + wrapper.setFeatures(Feature.MODIFICATION_DETECTION, Feature.LIBREPGP_OCB_ENCRYPTED_DATA); + PGPSignatureSubpacketVector vector = SignatureSubpacketsHelper.toVector(wrapper); + + Features features = vector.getFeatures(); + assertTrue(features.supportsModificationDetection()); + assertTrue(features.supportsFeature(Features.FEATURE_AEAD_ENCRYPTED_DATA)); + assertFalse(features.supportsFeature(Features.FEATURE_VERSION_5_PUBLIC_KEY)); + } + + @Test + public void testSetTrust() { + wrapper.setTrust(10, 5); + PGPSignatureSubpacketVector vector = SignatureSubpacketsHelper.toVector(wrapper); + + TrustSignature trustSignature = (TrustSignature) vector.getSubpacket(SignatureSubpacketTags.TRUST_SIG); + assertNotNull(trustSignature); + assertEquals(10, trustSignature.getDepth()); + assertEquals(5, trustSignature.getTrustAmount()); + } + + @Test + public void testAddEmbeddedSignature() throws PGPException, IOException { + PGPSecretKeyRing secretKeys = TestKeys.getEmilSecretKeyRing(); + Iterator secretKeyIterator = secretKeys.iterator(); + PGPSecretKey primaryKey = secretKeyIterator.next(); + PGPSignatureGenerator generator = new PGPSignatureGenerator( + ImplementationFactory.getInstance().getPGPContentSignerBuilder(primaryKey.getPublicKey().getAlgorithm(), HashAlgorithm.SHA512.getAlgorithmId()) + ); + + PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(primaryKey, (Passphrase) null); + generator.init(SignatureType.DIRECT_KEY.getCode(), privateKey); + PGPSignature sig1 = generator.generateCertification(primaryKey.getPublicKey()); + + generator.init(SignatureType.DIRECT_KEY.getCode(), privateKey); + PGPSignature sig2 = generator.generateCertification(secretKeyIterator.next().getPublicKey()); + + wrapper.addEmbeddedSignature(sig1); + + PGPSignatureSubpacketVector vector = SignatureSubpacketsHelper.toVector(wrapper); + assertEquals(1, vector.getEmbeddedSignatures().size()); + assertArrayEquals(sig1.getSignature(), vector.getEmbeddedSignatures().get(0).getSignature()); + + wrapper.addEmbeddedSignature(sig2); + + vector = SignatureSubpacketsHelper.toVector(wrapper); + assertEquals(2, vector.getEmbeddedSignatures().size()); + assertArrayEquals(sig2.getSignature(), vector.getEmbeddedSignatures().get(1).getSignature()); + + wrapper.clearEmbeddedSignatures(); + vector = SignatureSubpacketsHelper.toVector(wrapper); + assertEquals(0, vector.getEmbeddedSignatures().size()); + } + + @Test + public void testExtractSubpacketsFromVector() throws IOException { + Date sigCreationDate = DateUtil.parseUTCDate("2021-11-06 12:39:06 UTC"); + PGPPublicKeyRing publicKeys = TestKeys.getEmilPublicKeyRing(); + OpenPgpFingerprint fingerprint = OpenPgpFingerprint.of(publicKeys); + long keyId = fingerprint.getKeyId(); + + PGPSignatureSubpacketGenerator subpackets = new PGPSignatureSubpacketGenerator(); + // These are not extracted from the vector + subpackets.setSignatureCreationTime(true, sigCreationDate); + subpackets.setIssuerKeyID(true, keyId); + subpackets.setIssuerFingerprint(true, publicKeys.getPublicKey()); + // These are extracted + subpackets.setSignatureExpirationTime(true, 256000); + subpackets.setExportable(true, true); + subpackets.setTrust(true, 5, 15); + subpackets.setRevocable(true, true); + subpackets.setKeyExpirationTime(true, 512000); + subpackets.setPreferredSymmetricAlgorithms(true, new int[] { + SymmetricKeyAlgorithm.AES_192.getAlgorithmId(), SymmetricKeyAlgorithm.AES_128.getAlgorithmId() + }); + subpackets.addRevocationKey(true, publicKeys.getPublicKey().getAlgorithm(), + publicKeys.getPublicKey().getFingerprint()); + subpackets.addNotationData(false, true, "test@test.test", "test"); + subpackets.addNotationData(false, true, "check@check.check", "check"); + subpackets.setPreferredHashAlgorithms(true, new int[] { + HashAlgorithm.SHA512.getAlgorithmId(), HashAlgorithm.SHA384.getAlgorithmId() + }); + subpackets.setPreferredCompressionAlgorithms(true, new int[] { + CompressionAlgorithm.ZIP.getAlgorithmId(), CompressionAlgorithm.BZIP2.getAlgorithmId() + }); + subpackets.setPrimaryUserID(true, true); + subpackets.setKeyFlags(true, KeyFlag.toBitmask(KeyFlag.SIGN_DATA, KeyFlag.CERTIFY_OTHER)); + subpackets.addSignerUserID(false, "alice@test.test"); + subpackets.setRevocationReason(true, RevocationAttributes.Reason.KEY_RETIRED.code(), "Key was retired."); + subpackets.setFeature(true, Feature.toBitmask(Feature.MODIFICATION_DETECTION, Feature.LIBREPGP_OCB_ENCRYPTED_DATA)); + byte[] hash = new byte[128]; + new Random().nextBytes(hash); + subpackets.setSignatureTarget(false, publicKeys.getPublicKey().getAlgorithm(), HashAlgorithm.SHA512.getAlgorithmId(), hash); + subpackets.addIntendedRecipientFingerprint(true, publicKeys.getPublicKey()); + PreferredAlgorithms aead = new PreferredAlgorithms(SignatureSubpacketTags.PREFERRED_AEAD_ALGORITHMS, false, new int[] {2}); + subpackets.addCustomSubpacket(aead); + + + SignatureSubpackets wrapper = SignatureSubpackets.createSubpacketsFrom(subpackets.generate()); + PGPSignatureSubpacketVector vector = SignatureSubpacketsHelper.toVector(wrapper); + + // Verify these are not extracted + assertEquals(0, vector.getIssuerKeyID()); + assertNull(vector.getIssuerFingerprint()); + assertNull(vector.getSignatureCreationTime()); + + // Verify these are extracted + assertEquals(256000, vector.getSignatureExpirationTime()); + assertTrue(((Exportable) vector.getSubpacket(SignatureSubpacketTags.EXPORTABLE)).isExportable()); + TrustSignature trust = (TrustSignature) vector.getSubpacket(SignatureSubpacketTags.TRUST_SIG); + assertEquals(5, trust.getDepth()); + assertEquals(15, trust.getTrustAmount()); + assertTrue(((Revocable) vector.getSubpacket(SignatureSubpacketTags.REVOCABLE)).isRevocable()); + assertEquals(512000, vector.getKeyExpirationTime()); + assertArrayEquals(new int[] { + SymmetricKeyAlgorithm.AES_192.getAlgorithmId(), SymmetricKeyAlgorithm.AES_128.getAlgorithmId() + }, vector.getPreferredSymmetricAlgorithms()); + assertArrayEquals(publicKeys.getPublicKey().getFingerprint(), + ((RevocationKey) vector.getSubpacket(SignatureSubpacketTags.REVOCATION_KEY)).getFingerprint()); + assertEquals(2, vector.getNotationDataOccurrences().length); + assertEquals("test@test.test", vector.getNotationDataOccurrences()[0].getNotationName()); + assertEquals("test", vector.getNotationDataOccurrences()[0].getNotationValue()); + assertEquals("check@check.check", vector.getNotationDataOccurrences()[1].getNotationName()); + assertEquals("check", vector.getNotationDataOccurrences()[1].getNotationValue()); + assertArrayEquals(new int[] { + HashAlgorithm.SHA512.getAlgorithmId(), HashAlgorithm.SHA384.getAlgorithmId() + }, vector.getPreferredHashAlgorithms()); + assertArrayEquals(new int[] { + CompressionAlgorithm.ZIP.getAlgorithmId(), CompressionAlgorithm.BZIP2.getAlgorithmId() + }, vector.getPreferredCompressionAlgorithms()); + assertTrue(vector.isPrimaryUserID()); + assertEquals(KeyFlag.toBitmask(KeyFlag.SIGN_DATA, KeyFlag.CERTIFY_OTHER), vector.getKeyFlags()); + assertEquals("alice@test.test", vector.getSignerUserID()); + RevocationReason reason = (RevocationReason) vector.getSubpacket(SignatureSubpacketTags.REVOCATION_REASON); + assertEquals(RevocationAttributes.Reason.KEY_RETIRED.code(), reason.getRevocationReason()); + assertEquals("Key was retired.", reason.getRevocationDescription()); + assertTrue(vector.getFeatures().supportsFeature(Features.FEATURE_MODIFICATION_DETECTION)); + assertTrue(vector.getFeatures().supportsFeature(Features.FEATURE_AEAD_ENCRYPTED_DATA)); + SignatureTarget signatureTarget = vector.getSignatureTarget(); + assertEquals(publicKeys.getPublicKey().getAlgorithm(), signatureTarget.getPublicKeyAlgorithm()); + assertEquals(HashAlgorithm.SHA512.getAlgorithmId(), signatureTarget.getHashAlgorithm()); + assertArrayEquals(hash, signatureTarget.getHashData()); + assertArrayEquals(publicKeys.getPublicKey().getFingerprint(), vector.getIntendedRecipientFingerprint().getFingerprint()); + PreferredAlgorithms aeadAlgorithms = (PreferredAlgorithms) vector.getSubpacket(SignatureSubpacketTags.PREFERRED_AEAD_ALGORITHMS); + assertArrayEquals(aead.getPreferences(), aeadAlgorithms.getPreferences()); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/symmetric_encryption/MultiPassphraseSymmetricEncryptionTest.java b/pgpainless-core/src/test/java/org/pgpainless/symmetric_encryption/MultiPassphraseSymmetricEncryptionTest.java index 9b26921f..d0d37117 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/symmetric_encryption/MultiPassphraseSymmetricEncryptionTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/symmetric_encryption/MultiPassphraseSymmetricEncryptionTest.java @@ -11,23 +11,22 @@ import java.nio.charset.StandardCharsets; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.util.io.Streams; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; import org.pgpainless.PGPainless; import org.pgpainless.decryption_verification.ConsumerOptions; import org.pgpainless.decryption_verification.DecryptionStream; import org.pgpainless.encryption_signing.EncryptionOptions; import org.pgpainless.encryption_signing.EncryptionStream; import org.pgpainless.encryption_signing.ProducerOptions; -import org.pgpainless.implementation.ImplementationFactory; +import org.pgpainless.util.TestAllImplementations; import org.pgpainless.util.Passphrase; public class MultiPassphraseSymmetricEncryptionTest { - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void encryptDecryptWithMultiplePassphrases(ImplementationFactory implementationFactory) throws IOException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void encryptDecryptWithMultiplePassphrases() throws IOException, PGPException { String message = "Here we test if during decryption of a message that was encrypted with two passphrases, " + "the decryptor finds the session key encrypted for the right passphrase."; ByteArrayInputStream plaintextIn = new ByteArrayInputStream(message.getBytes(StandardCharsets.UTF_8)); @@ -36,8 +35,8 @@ public class MultiPassphraseSymmetricEncryptionTest { .onOutputStream(ciphertextOut) .withOptions(ProducerOptions.encrypt( EncryptionOptions.encryptCommunications() - .addPassphrase(Passphrase.fromPassword("p1")) - .addPassphrase(Passphrase.fromPassword("p2")) + .addMessagePassphrase(Passphrase.fromPassword("p1")) + .addMessagePassphrase(Passphrase.fromPassword("p2")) ).setAsciiArmor(false)); Streams.pipeAll(plaintextIn, encryptor); @@ -50,7 +49,7 @@ public class MultiPassphraseSymmetricEncryptionTest { DecryptionStream decryptor = PGPainless.decryptAndOrVerify() .onInputStream(new ByteArrayInputStream(ciphertext)) .withOptions(new ConsumerOptions() - .addDecryptionPassphrase(passphrase)); + .addMessagePassphrase(passphrase)); ByteArrayOutputStream plaintextOut = new ByteArrayOutputStream(); diff --git a/pgpainless-core/src/test/java/org/pgpainless/symmetric_encryption/SymmetricEncryptionTest.java b/pgpainless-core/src/test/java/org/pgpainless/symmetric_encryption/SymmetricEncryptionTest.java index 68f929af..dbf7ca24 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/symmetric_encryption/SymmetricEncryptionTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/symmetric_encryption/SymmetricEncryptionTest.java @@ -17,8 +17,9 @@ import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; import org.bouncycastle.util.io.Streams; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; +import org.pgpainless.util.TestAllImplementations; import org.pgpainless.PGPainless; import org.pgpainless.decryption_verification.ConsumerOptions; import org.pgpainless.decryption_verification.DecryptionStream; @@ -27,7 +28,6 @@ import org.pgpainless.encryption_signing.EncryptionOptions; import org.pgpainless.encryption_signing.EncryptionStream; import org.pgpainless.encryption_signing.ProducerOptions; import org.pgpainless.exception.MissingDecryptionMethodException; -import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.TestKeys; import org.pgpainless.key.protection.KeyRingProtectionSettings; import org.pgpainless.key.protection.PasswordBasedSecretKeyRingProtector; @@ -40,10 +40,9 @@ import org.pgpainless.util.Passphrase; */ public class SymmetricEncryptionTest { - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void encryptWithKeyAndPassphrase_DecryptWithKey(ImplementationFactory implementationFactory) throws IOException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void encryptWithKeyAndPassphrase_DecryptWithKey() throws IOException, PGPException { byte[] plaintext = "This is a secret message".getBytes(StandardCharsets.UTF_8); ByteArrayInputStream plaintextIn = new ByteArrayInputStream(plaintext); PGPPublicKeyRing encryptionKey = TestKeys.getCryptiePublicKeyRing(); @@ -54,7 +53,7 @@ public class SymmetricEncryptionTest { .onOutputStream(ciphertextOut) .withOptions(ProducerOptions.encrypt( EncryptionOptions.encryptCommunications() - .addPassphrase(encryptionPassphrase) + .addMessagePassphrase(encryptionPassphrase) .addRecipient(encryptionKey) )); @@ -67,7 +66,7 @@ public class SymmetricEncryptionTest { DecryptionStream decryptor = PGPainless.decryptAndOrVerify() .onInputStream(new ByteArrayInputStream(ciphertext)) .withOptions(new ConsumerOptions() - .addDecryptionPassphrase(encryptionPassphrase)); + .addMessagePassphrase(encryptionPassphrase)); ByteArrayOutputStream decrypted = new ByteArrayOutputStream(); @@ -94,11 +93,9 @@ public class SymmetricEncryptionTest { assertArrayEquals(plaintext, decrypted.toByteArray()); } - @ParameterizedTest - @MethodSource("org.pgpainless.util.TestImplementationFactoryProvider#provideImplementationFactories") - public void testMismatchPassphraseFails(ImplementationFactory implementationFactory) throws IOException, PGPException { - ImplementationFactory.setFactoryImplementation(implementationFactory); - + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void testMismatchPassphraseFails() throws IOException, PGPException { byte[] bytes = new byte[5000]; new Random().nextBytes(bytes); @@ -106,7 +103,7 @@ public class SymmetricEncryptionTest { EncryptionStream encryptor = PGPainless.encryptAndOrSign().onOutputStream(ciphertextOut) .withOptions(ProducerOptions.encrypt( EncryptionOptions.encryptCommunications() - .addPassphrase(Passphrase.fromPassword("mellon")))); + .addMessagePassphrase(Passphrase.fromPassword("mellon")))); Streams.pipeAll(new ByteArrayInputStream(bytes), encryptor); encryptor.close(); @@ -115,6 +112,6 @@ public class SymmetricEncryptionTest { .onInputStream(new ByteArrayInputStream(ciphertextOut.toByteArray())) .withOptions(new ConsumerOptions() .setMissingKeyPassphraseStrategy(MissingKeyPassphraseStrategy.THROW_EXCEPTION) - .addDecryptionPassphrase(Passphrase.fromPassword("meldir")))); + .addMessagePassphrase(Passphrase.fromPassword("meldir")))); } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/timeframe/TestTimeFrameProvider.java b/pgpainless-core/src/test/java/org/pgpainless/timeframe/TestTimeFrameProvider.java new file mode 100644 index 00000000..9d32a405 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/timeframe/TestTimeFrameProvider.java @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.timeframe; + +import java.util.Calendar; +import java.util.Date; +import java.util.TimeZone; + +public class TestTimeFrameProvider { + + /** + * Return an expiration date which is 7h 13m and 31s from the given date. + * + * @param now t0 + * @return t1 which is t0 +7h13m31s + */ + public static Date defaultExpirationForCreationDate(Date now) { + Calendar calendar = Calendar.getInstance(); + calendar.setTimeZone(TimeZone.getTimeZone("UTC")); + calendar.setTime(now); + calendar.add(Calendar.HOUR, 7); + calendar.add(Calendar.MINUTE, 13); + calendar.add(Calendar.SECOND, 31); + return calendar.getTime(); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/timeframe/package-info.java b/pgpainless-core/src/test/java/org/pgpainless/timeframe/package-info.java new file mode 100644 index 00000000..dfada70c --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/timeframe/package-info.java @@ -0,0 +1,8 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +/** + * Test classes for timeframes. + */ +package org.pgpainless.timeframe; diff --git a/pgpainless-core/src/test/java/org/pgpainless/util/ArmorUtilsTest.java b/pgpainless-core/src/test/java/org/pgpainless/util/ArmorUtilsTest.java index b9610892..c0a6e00c 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/util/ArmorUtilsTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/util/ArmorUtilsTest.java @@ -21,14 +21,26 @@ import org.bouncycastle.openpgp.PGPCompressedData; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPLiteralData; import org.bouncycastle.openpgp.PGPObjectFactory; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.PGPUtil; -import org.bouncycastle.openpgp.bc.BcPGPObjectFactory; import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; +import org.pgpainless.PGPainless; import org.pgpainless.algorithm.HashAlgorithm; +import org.pgpainless.algorithm.KeyFlag; +import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.TestKeys; +import org.pgpainless.key.generation.KeySpec; +import org.pgpainless.key.generation.type.ecc.EllipticCurve; +import org.pgpainless.key.generation.type.ecc.ecdsa.ECDSA; +import org.pgpainless.signature.SignatureUtils; public class ArmorUtilsTest { @@ -70,7 +82,7 @@ public class ArmorUtilsTest { assertEquals(HashAlgorithm.SHA512, hashes.get(0)); // Comment - List commentHeader = ArmorUtils.getCommendHeaderValues(armorIn); + List commentHeader = ArmorUtils.getCommentHeaderValues(armorIn); assertEquals(2, commentHeader.size()); assertEquals("This is a comment", commentHeader.get(0)); assertEquals("This is another comment", commentHeader.get(1)); @@ -84,7 +96,6 @@ public class ArmorUtilsTest { ArmoredOutputStream armor = new ArmoredOutputStream(new ByteArrayOutputStream()); ArmorUtils.addMessageIdHeader(armor, "abcdefghi01234567890123456789012"); - assertThrows(NullPointerException.class, () -> ArmorUtils.addMessageIdHeader(armor, null)); assertThrows(IllegalArgumentException.class, () -> ArmorUtils.addMessageIdHeader(armor, "tooShort")); assertThrows(IllegalArgumentException.class, () -> ArmorUtils.addMessageIdHeader(armor, "toLooooooooooooooooooooooooooooooooooong")); assertThrows(IllegalArgumentException.class, () -> ArmorUtils.addMessageIdHeader(armor, "contains spaces 7890123456789012")); @@ -118,6 +129,29 @@ public class ArmorUtilsTest { assertTrue(ascii.startsWith("-----BEGIN PGP PRIVATE KEY BLOCK-----\n")); } + @Test + public void signatureToAsciiArmoredString() throws PGPException, IOException { + String SIG = "-----BEGIN PGP SIGNATURE-----\n" + + "Version: PGPainless\n" + + "\n" + + "iHUEARMKAB0WIQRPZlxNwsRmC8ZCXkFXNuaTGs83DAUCYJ/x5gAKCRBXNuaTGs83\n" + + "DFRwAP9/4wMvV3WcX59Clo7mkRce6iwW3VBdiN+yMu3tjmHB2wD/RfE28Q1v4+eo\n" + + "ySNgbyvqYYsNr0fnBwaG3aaj+u5ExiE=\n" + + "=Z2SO\n" + + "-----END PGP SIGNATURE-----\n"; + PGPSignature signature = SignatureUtils.readSignatures(SIG).get(0); + String armored = PGPainless.asciiArmor(signature); + assertEquals(SIG, armored); + } + + @Test + public void testAsciiArmorToStream() throws IOException, PGPException { + PGPSecretKeyRing secretKeys = TestKeys.getEmilSecretKeyRing(); + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + PGPainless.asciiArmor(secretKeys, bytes); + assertTrue(bytes.toString().startsWith("-----BEGIN PGP PRIVATE KEY BLOCK-----\n")); + } + @Test public void testSetCustomVersionHeader() throws IOException { ArmoredOutputStreamFactory.setVersionInfo("MyVeryFirstOpenPGPProgram 1.0"); @@ -143,6 +177,73 @@ public class ArmorUtilsTest { } @Test + public void testMultipleIdentitiesInHeader() throws Exception { + PGPSecretKeyRing secretKeyRing = PGPainless.buildKeyRing() + .setPrimaryKey(KeySpec.getBuilder(ECDSA.fromCurve(EllipticCurve._P256), KeyFlag.SIGN_DATA, KeyFlag.CERTIFY_OTHER)) + .addUserId("Juliet ") + .addUserId("xmpp:juliet@capulet.lit") + .setPassphrase(Passphrase.fromPassword("test")) + .build(); + PGPPublicKey publicKey = secretKeyRing.getPublicKey(); + PGPPublicKeyRing publicKeyRing = PGPainless.readKeyRing().publicKeyRing(publicKey.getEncoded()); + String armored = PGPainless.asciiArmor(publicKeyRing); + Assertions.assertTrue(armored.contains("Comment: Juliet ")); + Assertions.assertTrue(armored.contains("Comment: 1 further identity")); + } + + @Test + public void testEvenMoreIdentitiesInHeader() throws Exception { + PGPSecretKeyRing secretKeyRing = PGPainless.buildKeyRing() + .setPrimaryKey(KeySpec.getBuilder(ECDSA.fromCurve(EllipticCurve._P256), KeyFlag.SIGN_DATA, KeyFlag.CERTIFY_OTHER)) + .addUserId("Juliet ") + .addUserId("xmpp:juliet@capulet.lit") + .addUserId("Juliet Montague ") + .setPassphrase(Passphrase.fromPassword("test")) + .build(); + PGPPublicKey publicKey = secretKeyRing.getPublicKey(); + PGPPublicKeyRing publicKeyRing = PGPainless.readKeyRing().publicKeyRing(publicKey.getEncoded()); + String armored = PGPainless.asciiArmor(publicKeyRing); + Assertions.assertTrue(armored.contains("Comment: Juliet ")); + Assertions.assertTrue(armored.contains("Comment: 2 further identities")); + } + + + @Test + public void testSingleIdentityInHeader() throws Exception { + PGPSecretKeyRing secretKeyRing = PGPainless.buildKeyRing() + .setPrimaryKey(KeySpec.getBuilder(ECDSA.fromCurve(EllipticCurve._P256), KeyFlag.SIGN_DATA, KeyFlag.CERTIFY_OTHER)) + .addUserId("Juliet ") + .setPassphrase(Passphrase.fromPassword("test")) + .build(); + PGPPublicKey publicKey = secretKeyRing.getPublicKey(); + PGPPublicKeyRing publicKeyRing = PGPainless.readKeyRing().publicKeyRing(publicKey.getEncoded()); + String armored = PGPainless.asciiArmor(publicKeyRing); + Assertions.assertTrue(armored.contains("Comment: Juliet ")); + Assertions.assertFalse(armored.contains("Comment: 1 total identities")); + } + + @Test + public void testWithoutIdentityInHeader() throws Exception { + final String CERT = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "\n" + + "xsBNBGIgzE0BCACwxaYg6bpmp0POq1T6yalGE9XaL2IG9d9khDBweZ63s3Pu1pHB\n" + + "JtmjgN7Tx3ts6hLzQm3YKYA6zu1MXQ8k2vqtdtGUpZPp18Pbars7yUDqh8QIdFjO\n" + + "GeE+c8So0MQgTgoBuyZiSmslwp1WO78ozf/0rCayFdy73dPUntuLE6c2ZKO8nw/g\n" + + "uyk2ozsqLN/TBpgbuJUyMedJtXV10DdT9QxH/66LmdjFKXTkc74qI8YAm/pmJeOh\n" + + "36qZ5ehAgz9MthPQINnZKpnqidqkGFvjwVFlCMlVSmNCNJmpgGDH3gvkklZHzGsf\n" + + "dfzQswd/BQjPsFH9cK+QFYMG6q2zrvM0X9mdABEBAAE=\n" + + "=njg8\n" + + "-----END PGP PUBLIC KEY BLOCK-----\n"; + + PGPPublicKeyRing publicKeys = PGPainless.readKeyRing().publicKeyRing(CERT); + PGPPublicKey publicKey = publicKeys.getPublicKey(); + PGPPublicKeyRing publicKeyRing = PGPainless.readKeyRing().publicKeyRing(publicKey.getEncoded()); + String armored = PGPainless.asciiArmor(publicKeyRing); + Assertions.assertFalse(armored.contains("Comment: 0 total identities")); + } + + @TestTemplate + @ExtendWith(TestAllImplementations.class) public void decodeExampleTest() throws IOException, PGPException { String armored = "-----BEGIN PGP MESSAGE-----\n" + "Version: OpenPrivacy 0.99\n" + @@ -152,9 +253,10 @@ public class ArmorUtilsTest { "=njUN\n" + "-----END PGP MESSAGE-----"; InputStream inputStream = PGPUtil.getDecoderStream(new ByteArrayInputStream(armored.getBytes(StandardCharsets.UTF_8))); - PGPObjectFactory factory = new BcPGPObjectFactory(inputStream); + + PGPObjectFactory factory = ImplementationFactory.getInstance().getPGPObjectFactory(inputStream); PGPCompressedData compressed = (PGPCompressedData) factory.nextObject(); - factory = new BcPGPObjectFactory(compressed.getDataStream()); + factory = ImplementationFactory.getInstance().getPGPObjectFactory(compressed.getDataStream()); PGPLiteralData literal = (PGPLiteralData) factory.nextObject(); ByteArrayOutputStream out = new ByteArrayOutputStream(); assertEquals("_CONSOLE", literal.getFileName()); diff --git a/pgpainless-core/src/test/java/org/pgpainless/util/ArmoredInputStreamFactoryTest.java b/pgpainless-core/src/test/java/org/pgpainless/util/ArmoredInputStreamFactoryTest.java new file mode 100644 index 00000000..95e3961b --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/util/ArmoredInputStreamFactoryTest.java @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.util; + +import org.bouncycastle.bcpg.ArmoredInputStream; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class ArmoredInputStreamFactoryTest { + + // Hello World!\n + String armored = "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "owE7LZzEAAIeqTk5+Qrh+UU5KYpcAA==\n" + + "=g3nV\n" + + "-----END PGP MESSAGE-----"; + + @Test + public void testGet() throws IOException { + ByteArrayInputStream inputStream = new ByteArrayInputStream(armored.getBytes()); + ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(inputStream); + assertNotNull(armorIn); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/util/BCUtilTest.java b/pgpainless-core/src/test/java/org/pgpainless/util/BCUtilTest.java index ab61b829..821d9e30 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/util/BCUtilTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/util/BCUtilTest.java @@ -5,8 +5,9 @@ package org.pgpainless.util; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; -import java.io.IOException; import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; import java.util.Iterator; @@ -17,6 +18,7 @@ import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; +import org.bouncycastle.util.Arrays; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.KeyFlag; @@ -33,9 +35,8 @@ public class BCUtilTest { @Test public void keyRingToCollectionTest() - throws PGPException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, - IOException { - PGPSecretKeyRing sec = PGPainless.generateKeyRing() + throws PGPException, NoSuchAlgorithmException, InvalidAlgorithmParameterException { + PGPSecretKeyRing sec = PGPainless.buildKeyRing() .setPrimaryKey(KeySpec.getBuilder( KeyType.RSA(RsaLength._3072), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA)) @@ -88,4 +89,18 @@ public class BCUtilTest { assertEquals(pubColSize, secColSize); } + + @Test + public void constantTimeAreEqualsTest() { + char[] b = "Hello".toCharArray(); + assertTrue(Arrays.constantTimeAreEqual(b, b)); + assertTrue(Arrays.constantTimeAreEqual("Hello".toCharArray(), "Hello".toCharArray())); + assertTrue(Arrays.constantTimeAreEqual(new char[0], new char[0])); + assertTrue(Arrays.constantTimeAreEqual(new char[] {'H', 'e', 'l', 'l', 'o'}, "Hello".toCharArray())); + + assertFalse(Arrays.constantTimeAreEqual("Hello".toCharArray(), "Hello World".toCharArray())); + assertFalse(Arrays.constantTimeAreEqual(null, "Hello".toCharArray())); + assertFalse(Arrays.constantTimeAreEqual("Hello".toCharArray(), null)); + assertFalse(Arrays.constantTimeAreEqual((char[]) null, null)); + } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/util/CollectionUtils.java b/pgpainless-core/src/test/java/org/pgpainless/util/CollectionUtils.java new file mode 100644 index 00000000..8fcac2ce --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/util/CollectionUtils.java @@ -0,0 +1,80 @@ +// SPDX-FileCopyrightText: 2021 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.util; + +import javax.annotation.Nonnull; +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; + +public final class CollectionUtils { + + private CollectionUtils() { + + } + + /** + * Return all items returned by the {@link Iterator} as a {@link List}. + * + * @param iterator iterator + * @param type + * @return list + */ + @Nonnull + public static List iteratorToList(@Nonnull Iterator iterator) { + List items = new ArrayList<>(); + while (iterator.hasNext()) { + I item = iterator.next(); + items.add(item); + } + return items; + } + + /** + * Return a new array which contains
t
as first element, followed by the elements of
ts
. + * @param t head + * @param ts tail + * @param type + * @return t and ts as array + */ + @Nonnull + public static T[] concat(@Nonnull T t, @Nonnull T[] ts) { + T[] concat = (T[]) Array.newInstance(t.getClass(), ts.length + 1); + concat[0] = t; + System.arraycopy(ts, 0, concat, 1, ts.length); + return concat; + } + + /** + * Return true, if the given array
ts
contains the element
t
. + * @param ts elements + * @param t searched element + * @param type + * @return true if ts contains t, false otherwise + */ + public static boolean contains(@Nonnull T[] ts, @Nonnull T t) { + for (T i : ts) { + if (i.equals(t)) { + return true; + } + } + return false; + } + + /** + * Add all items from the iterator to the collection. + * + * @param type of item + * @param iterator iterator to gather items from + * @param collection collection to add items to + */ + public static void addAll(@Nonnull Iterator iterator, @Nonnull Collection collection) { + while (iterator.hasNext()) { + collection.add(iterator.next()); + } + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/util/CollectionUtilsTest.java b/pgpainless-core/src/test/java/org/pgpainless/util/CollectionUtilsTest.java new file mode 100644 index 00000000..2bfe6cb1 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/util/CollectionUtilsTest.java @@ -0,0 +1,112 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.util; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import org.junit.jupiter.api.Test; + +public class CollectionUtilsTest { + + @Test + public void testConcat() { + String a = "A"; + String[] bc = new String[] {"B", "C"}; + + String[] abc = CollectionUtils.concat(a, bc); + assertArrayEquals(new String[] {"A", "B", "C"}, abc); + } + + @Test + public void testConcatWithEmptyArray() { + String a = "A"; + String[] empty = new String[0]; + + String[] concat = CollectionUtils.concat(a, empty); + assertArrayEquals(new String[] {"A"}, concat); + } + + @Test + public void iteratorToListTest() { + List list = Arrays.asList("A", "B", "C"); + Iterator iterator = list.iterator(); + + List listFromIterator = CollectionUtils.iteratorToList(iterator); + assertEquals(list, listFromIterator); + } + + @Test + public void iteratorToList_emptyIteratorTest() { + Iterator iterator = Collections.emptyIterator(); + + List listFromIterator = CollectionUtils.iteratorToList(iterator); + assertTrue(listFromIterator.isEmpty()); + } + + @Test + public void containsTest() { + String[] abc = new String[] {"A", "B", "C"}; + + assertTrue(CollectionUtils.contains(abc, "A")); + assertTrue(CollectionUtils.contains(abc, "B")); + assertTrue(CollectionUtils.contains(abc, "C")); + assertFalse(CollectionUtils.contains(abc, "D")); + } + + @Test + public void contains_emptyTest() { + String[] empty = new String[0]; + + assertFalse(CollectionUtils.contains(empty, "A")); + } + + @Test + public void addAllTest() { + List list = new ArrayList<>(); + list.add("A"); + list.add("B"); + + List other = new ArrayList<>(); + other.add("C"); + other.add("D"); + Iterator iterator = other.iterator(); + + CollectionUtils.addAll(iterator, list); + + assertEquals(Arrays.asList("A", "B", "C", "D"), list); + } + + @Test + public void addAllEmptyListTest() { + List empty = new ArrayList<>(); + + List other = Arrays.asList("A", "B", "C"); + Iterator iterator = other.iterator(); + + CollectionUtils.addAll(iterator, empty); + assertEquals(Arrays.asList("A", "B", "C"), empty); + } + + @Test + public void addAllEmptyIterator() { + List list = new ArrayList<>(); + list.add("A"); + list.add("B"); + + Iterator iterator = Collections.emptyIterator(); + + CollectionUtils.addAll(iterator, list); + assertEquals(Arrays.asList("A", "B"), list); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/util/GuessPreferredHashAlgorithmTest.java b/pgpainless-core/src/test/java/org/pgpainless/util/GuessPreferredHashAlgorithmTest.java index 76c269ea..e276ba8f 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/util/GuessPreferredHashAlgorithmTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/util/GuessPreferredHashAlgorithmTest.java @@ -6,7 +6,6 @@ package org.pgpainless.util; import static org.junit.jupiter.api.Assertions.assertEquals; -import java.io.IOException; import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; import java.util.Collections; @@ -22,15 +21,16 @@ import org.pgpainless.algorithm.KeyFlag; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.key.generation.KeySpec; import org.pgpainless.key.generation.type.KeyType; -import org.pgpainless.key.generation.type.eddsa.EdDSACurve; +import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve; import org.pgpainless.key.util.OpenPgpKeyAttributeUtil; public class GuessPreferredHashAlgorithmTest { @Test - public void guessPreferredHashAlgorithmsAssumesHashAlgoUsedBySelfSig() throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException, IOException { - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() - .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), + public void guessPreferredHashAlgorithmsAssumesHashAlgoUsedBySelfSig() + throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { + PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA) .overridePreferredHashAlgorithms(new HashAlgorithm[] {}) .overridePreferredSymmetricKeyAlgorithms(new SymmetricKeyAlgorithm[] {}) diff --git a/pgpainless-core/src/test/java/org/pgpainless/util/MultiMapTest.java b/pgpainless-core/src/test/java/org/pgpainless/util/MultiMapTest.java index 74c3347f..164befb1 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/util/MultiMapTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/util/MultiMapTest.java @@ -14,6 +14,8 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.Set; import org.junit.jupiter.api.Test; @@ -39,7 +41,37 @@ public class MultiMapTest { assertTrue(multiMap.containsKey("alice")); assertTrue(multiMap.containsValue("wonderland")); assertNotNull(multiMap.get("alice")); - assertTrue(multiMap.get("alice").contains("wonderland")); + assertTrue(multiMap.contains("alice", "wonderland")); + } + + @Test + public void putOverwritesExistingElements() { + MultiMap map = new MultiMap<>(); + map.put("alice", "wonderland"); + map.put("alice", "whothefrickisalice"); + assertFalse(map.containsValue("wonderland")); + } + + @Test + public void plusDoesNotOverwriteButAdd() { + MultiMap map = new MultiMap<>(); + map.put("alice", "wonderland"); + map.plus("alice", "whothefrickisalice"); + assertTrue(map.containsValue("wonderland")); + assertTrue(map.containsValue("whothefrickisalice")); + } + + @Test + public void containsWorks() { + MultiMap map = new MultiMap<>(); + map.put("alice", "wonderland"); + map.plus("alice", "bar"); + map.put("bob", "builder"); + + assertTrue(map.contains("alice", "wonderland")); + assertTrue(map.contains("alice", "bar")); + assertTrue(map.contains("bob", "builder")); + assertFalse(map.contains("bob", "bar")); } @Test @@ -102,7 +134,7 @@ public class MultiMapTest { @Test public void emptyAfterClear() { MultiMap map = new MultiMap<>(); - map.put("test", "foo"); + map.plus("test", "foo"); assertFalse(map.isEmpty()); map.clear(); assertTrue(map.isEmpty()); @@ -111,8 +143,8 @@ public class MultiMapTest { @Test public void addTwoRemoveOneWorks() { MultiMap map = new MultiMap<>(); - map.put("alice", "wonderland"); - map.put("bob", "builder"); + map.plus("alice", "wonderland"); + map.plus("bob", "builder"); map.removeAll("alice"); assertFalse(map.containsKey("alice")); @@ -123,11 +155,11 @@ public class MultiMapTest { @Test public void addMultiValue() { MultiMap addOneByOne = new MultiMap<>(); - addOneByOne.put("foo", "bar"); - addOneByOne.put("foo", "baz"); + addOneByOne.plus("foo", "bar"); + addOneByOne.plus("foo", "baz"); MultiMap addOnce = new MultiMap<>(); - addOnce.put("foo", new HashSet<>(Arrays.asList("baz", "bar"))); + addOnce.plus("foo", new HashSet<>(Arrays.asList("baz", "bar"))); assertEquals(addOneByOne, addOnce); } @@ -136,7 +168,7 @@ public class MultiMapTest { public void addMultiValueRemoveSingle() { MultiMap map = new MultiMap<>(); map.put("foo", "bar"); - map.put("foo", "baz"); + map.plus("foo", "baz"); map.remove("foo", "bar"); assertFalse(map.isEmpty()); @@ -147,13 +179,52 @@ public class MultiMapTest { @Test public void addMultiValueRemoveAll() { MultiMap map = new MultiMap<>(); - map.put("foo", "bar"); - map.put("foo", "baz"); - map.put("bingo", "bango"); + map.plus("foo", "bar"); + map.plus("foo", "baz"); + map.plus("bingo", "bango"); map.removeAll("foo"); assertFalse(map.isEmpty()); assertFalse(map.containsKey("foo")); assertTrue(map.containsKey("bingo")); } + + @Test + public void plusAll() { + MultiMap map = new MultiMap<>(); + map.plus("A", "1"); + map.plus("A", "2"); + map.plus("B", "1"); + + MultiMap other = new MultiMap<>(); + other.plus("A", "1"); + other.plus("B", "2"); + other.plus("C", "3"); + + map.plusAll(other); + assertTrue(map.contains("A", "1")); + assertTrue(map.contains("A", "2")); + assertTrue(map.contains("B", "1")); + assertTrue(map.contains("B", "2")); + assertTrue(map.contains("C", "3")); + } + + @Test + public void flattenEmptyMap() { + MultiMap empty = new MultiMap<>(); + assertEquals(Collections.emptySet(), empty.flatten()); + } + + @Test + public void flattenMap() { + MultiMap map = new MultiMap<>(); + map.plus("A", "1"); + map.plus("A", "2"); + map.plus("B", "1"); + + Set expected = new LinkedHashSet<>(); + expected.add("1"); + expected.add("2"); + assertEquals(expected, map.flatten()); + } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/util/SignatureSubpacketGeneratorUtilTest.java b/pgpainless-core/src/test/java/org/pgpainless/util/SignatureSubpacketGeneratorUtilTest.java deleted file mode 100644 index fe10cf1e..00000000 --- a/pgpainless-core/src/test/java/org/pgpainless/util/SignatureSubpacketGeneratorUtilTest.java +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.util; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; - -import java.util.Date; - -import org.bouncycastle.bcpg.SignatureSubpacketTags; -import org.bouncycastle.bcpg.sig.Features; -import org.bouncycastle.openpgp.PGPSignatureSubpacketGenerator; -import org.bouncycastle.openpgp.PGPSignatureSubpacketVector; -import org.junit.jupiter.api.Test; -import org.pgpainless.algorithm.SignatureSubpacket; -import org.pgpainless.signature.subpackets.SignatureSubpacketGeneratorUtil; - -public class SignatureSubpacketGeneratorUtilTest { - - @Test - public void testRemoveAllPacketsOfTypeRemovesAll() { - PGPSignatureSubpacketGenerator generator = new PGPSignatureSubpacketGenerator(); - generator.setFeature(false, Features.FEATURE_MODIFICATION_DETECTION); - generator.setSignatureCreationTime(false, new Date()); - generator.setSignatureCreationTime(true, new Date()); - PGPSignatureSubpacketVector vector = generator.generate(); - - assertEquals(2, vector.getSubpackets(SignatureSubpacketTags.CREATION_TIME).length); - assertNotNull(vector.getSubpackets(SignatureSubpacketTags.FEATURES)); - - generator = new PGPSignatureSubpacketGenerator(vector); - SignatureSubpacketGeneratorUtil.removeAllPacketsOfType(SignatureSubpacket.signatureCreationTime, generator); - vector = generator.generate(); - - assertEquals(0, vector.getSubpackets(SignatureSubpacketTags.CREATION_TIME).length); - assertNotNull(vector.getSubpackets(SignatureSubpacketTags.FEATURES)); - } -} diff --git a/pgpainless-core/src/test/java/org/pgpainless/util/TestAllImplementations.java b/pgpainless-core/src/test/java/org/pgpainless/util/TestAllImplementations.java new file mode 100644 index 00000000..2b874b6d --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/util/TestAllImplementations.java @@ -0,0 +1,66 @@ +// SPDX-FileCopyrightText: 2021 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.util; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Stream; + +import org.junit.jupiter.api.extension.BeforeTestExecutionCallback; +import org.junit.jupiter.api.extension.Extension; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.TestTemplateInvocationContext; +import org.junit.jupiter.api.extension.TestTemplateInvocationContextProvider; +import org.pgpainless.implementation.BcImplementationFactory; +import org.pgpainless.implementation.ImplementationFactory; +import org.pgpainless.implementation.JceImplementationFactory; + +/** + * InvocationContextProvider that sets different {@link ImplementationFactory} implementations before running annotated + * tests. + * + * Example test annotation: + * {@code + * @TestTemplate + * @ExtendWith(ImplementationFactoryTestInvocationContextProvider.class) + * public void testAllImplementationFactories() { + * ... + * } + * } + * + * @see Baeldung: Writing Templates for Test Cases Using JUnit 5 + */ +public class TestAllImplementations implements TestTemplateInvocationContextProvider { + + private static final List IMPLEMENTATIONS = Arrays.asList( + new BcImplementationFactory(), + new JceImplementationFactory() + ); + + @Override + public boolean supportsTestTemplate(ExtensionContext context) { + return true; + } + + @Override + public Stream provideTestTemplateInvocationContexts(ExtensionContext context) { + + return IMPLEMENTATIONS.stream() + .map(implementationFactory -> new TestTemplateInvocationContext() { + @Override + public String getDisplayName(int invocationIndex) { + return context.getDisplayName() + " with " + implementationFactory.getClass().getSimpleName(); + } + + @Override + public List getAdditionalExtensions() { + return Collections.singletonList( + (BeforeTestExecutionCallback) ctx -> ImplementationFactory.setFactoryImplementation(implementationFactory) + ); + } + }); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/util/TestImplementationFactoryProvider.java b/pgpainless-core/src/test/java/org/pgpainless/util/TestImplementationFactoryProvider.java deleted file mode 100644 index 1ace7245..00000000 --- a/pgpainless-core/src/test/java/org/pgpainless/util/TestImplementationFactoryProvider.java +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.util; - -import org.pgpainless.implementation.BcImplementationFactory; -import org.pgpainless.implementation.ImplementationFactory; -import org.pgpainless.implementation.JceImplementationFactory; - -import java.util.Arrays; -import java.util.List; - -/** - * Utility class used to provide all available implementations of {@link ImplementationFactory} for parametrized tests. - */ -public class TestImplementationFactoryProvider { - - private static final List IMPLEMENTATIONS = Arrays.asList( - new BcImplementationFactory(), - new JceImplementationFactory() - ); - - public static List provideImplementationFactories() { - return IMPLEMENTATIONS; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/Tuple.java b/pgpainless-core/src/test/java/org/pgpainless/util/Tuple.java similarity index 66% rename from pgpainless-core/src/main/java/org/pgpainless/util/Tuple.java rename to pgpainless-core/src/test/java/org/pgpainless/util/Tuple.java index 27ad6a12..84d7a370 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/Tuple.java +++ b/pgpainless-core/src/test/java/org/pgpainless/util/Tuple.java @@ -4,6 +4,14 @@ package org.pgpainless.util; +/** + * Helper class pairing together two values. + * @param type of the first value + * @param type of the second value + * @deprecated Scheduled for removal. + * TODO: Remove + */ +@Deprecated public class Tuple { private final A a; diff --git a/pgpainless-core/src/test/java/org/pgpainless/util/selection/keyring/KeyRingsFromCollectionTest.java b/pgpainless-core/src/test/java/org/pgpainless/util/selection/keyring/KeyRingsFromCollectionTest.java index eb3510be..c7f6d722 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/util/selection/keyring/KeyRingsFromCollectionTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/util/selection/keyring/KeyRingsFromCollectionTest.java @@ -5,7 +5,7 @@ package org.pgpainless.util.selection.keyring; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.IOException; import java.util.Arrays; @@ -19,8 +19,8 @@ import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; import org.junit.jupiter.api.Test; import org.pgpainless.key.TestKeys; -import org.pgpainless.util.selection.keyring.impl.ExactUserId; import org.pgpainless.util.MultiMap; +import org.pgpainless.util.selection.keyring.impl.ExactUserId; public class KeyRingsFromCollectionTest { @@ -52,11 +52,11 @@ public class KeyRingsFromCollectionTest { MultiMap selected = strategy.selectKeyRingsFromCollections(map); assertEquals(1, selected.get(TestKeys.JULIET_UID).size()); assertEquals(1, selected.get(TestKeys.EMIL_UID).size()); - assertNull(selected.get("invalidId")); + assertTrue(selected.get("invalidId").isEmpty()); } @Test - public void selectPublicKeyRingFromPublicKeyRingCollectionTest() throws IOException, PGPException { + public void selectPublicKeyRingFromPublicKeyRingCollectionTest() throws IOException { PGPPublicKeyRing emil = TestKeys.getEmilPublicKeyRing(); PGPPublicKeyRing juliet = TestKeys.getJulietPublicKeyRing(); PGPPublicKeyRingCollection collection = new PGPPublicKeyRingCollection(Arrays.asList(emil, juliet)); @@ -68,21 +68,21 @@ public class KeyRingsFromCollectionTest { } @Test - public void selectPublicKeyRingMapFromPublicKeyRingCollectionMapTest() throws IOException, PGPException { + public void selectPublicKeyRingMapFromPublicKeyRingCollectionMapTest() throws IOException { PGPPublicKeyRing emil = TestKeys.getEmilPublicKeyRing(); PGPPublicKeyRing juliet = TestKeys.getJulietPublicKeyRing(); MultiMap map = new MultiMap<>(); PGPPublicKeyRingCollection julietCollection = new PGPPublicKeyRingCollection(Arrays.asList(emil, juliet)); - map.put(TestKeys.JULIET_UID, julietCollection); + map.plus(TestKeys.JULIET_UID, julietCollection); PGPPublicKeyRingCollection emilCollection = new PGPPublicKeyRingCollection(Collections.singletonList(emil)); - map.put(TestKeys.EMIL_UID, emilCollection); + map.plus(TestKeys.EMIL_UID, emilCollection); assertEquals(2, julietCollection.size()); - map.put("invalidId", emilCollection); + map.plus("invalidId", emilCollection); PublicKeyRingSelectionStrategy strategy = new ExactUserId.PubRingSelectionStrategy(); MultiMap selected = strategy.selectKeyRingsFromCollections(map); assertEquals(1, selected.get(TestKeys.JULIET_UID).size()); assertEquals(1, selected.get(TestKeys.EMIL_UID).size()); - assertNull(selected.get("invalidId")); + assertTrue(selected.get("invalidId").isEmpty()); } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/util/selection/keyring/WildcardKeyRingSelectionStrategyTest.java b/pgpainless-core/src/test/java/org/pgpainless/util/selection/keyring/WildcardKeyRingSelectionStrategyTest.java index 4346bd75..10907eca 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/util/selection/keyring/WildcardKeyRingSelectionStrategyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/util/selection/keyring/WildcardKeyRingSelectionStrategyTest.java @@ -18,8 +18,10 @@ import org.pgpainless.util.selection.keyring.impl.Wildcard; public class WildcardKeyRingSelectionStrategyTest { - Wildcard.PubRingSelectionStrategy pubKeySelectionStrategy = new Wildcard.PubRingSelectionStrategy<>(); - Wildcard.SecRingSelectionStrategy secKeySelectionStrategy = new Wildcard.SecRingSelectionStrategy<>(); + private static final Wildcard.PubRingSelectionStrategy pubKeySelectionStrategy + = new Wildcard.PubRingSelectionStrategy<>(); + private static final Wildcard.SecRingSelectionStrategy secKeySelectionStrategy + = new Wildcard.SecRingSelectionStrategy<>(); @Test public void testStratAcceptsMatchingUIDsOnPubKey() throws IOException { diff --git a/pgpainless-core/src/test/java/org/pgpainless/util/selection/keyring/XmppKeyRingSelectionStrategyTest.java b/pgpainless-core/src/test/java/org/pgpainless/util/selection/keyring/XmppKeyRingSelectionStrategyTest.java index 4de56a8a..2b5f8ebb 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/util/selection/keyring/XmppKeyRingSelectionStrategyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/util/selection/keyring/XmppKeyRingSelectionStrategyTest.java @@ -18,8 +18,10 @@ import org.pgpainless.util.selection.keyring.impl.XMPP; public class XmppKeyRingSelectionStrategyTest { - XMPP.PubRingSelectionStrategy pubKeySelectionStrategy = new XMPP.PubRingSelectionStrategy(); - XMPP.SecRingSelectionStrategy secKeySelectionStrategy = new XMPP.SecRingSelectionStrategy(); + private static final XMPP.PubRingSelectionStrategy pubKeySelectionStrategy = + new XMPP.PubRingSelectionStrategy(); + private static final XMPP.SecRingSelectionStrategy secKeySelectionStrategy = + new XMPP.SecRingSelectionStrategy(); @Test public void testMatchingXmppUIDAcceptedOnPubKey() throws IOException { diff --git a/pgpainless-core/src/test/java/org/pgpainless/util/selection/userid/SelectUserIdTest.java b/pgpainless-core/src/test/java/org/pgpainless/util/selection/userid/SelectUserIdTest.java index 6e7ea39c..99a0c87c 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/util/selection/userid/SelectUserIdTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/util/selection/userid/SelectUserIdTest.java @@ -4,6 +4,15 @@ package org.pgpainless.util.selection.userid; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.junit.jupiter.api.Test; @@ -11,19 +20,10 @@ import org.pgpainless.PGPainless; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.util.UserId; -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; - public class SelectUserIdTest { @Test - public void testSelectUserIds() throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { + public void testSelectUserIds() throws PGPException { PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() .simpleEcKeyRing(""); secretKeys = PGPainless.modifyKeyRing(secretKeys) @@ -33,48 +33,53 @@ public class SelectUserIdTest { SecretKeyRingProtector.unprotectedKeys()) .done(); - List validEmail = SelectUserId.and( + List userIds = PGPainless.inspectKeyRing(secretKeys).getValidUserIds(); + List validEmail = userIds.stream().filter(SelectUserId.and( SelectUserId.validUserId(secretKeys), SelectUserId.containsEmailAddress("alice@wonderland.lit") - ).selectUserIds(secretKeys); + )).collect(Collectors.toList()); assertEquals(Collections.singletonList(""), validEmail); - List startsWithAlice = SelectUserId.startsWith("Alice").selectUserIds(secretKeys); + List startsWithAlice = userIds.stream().filter(SelectUserId.startsWith("Alice")).collect(Collectors.toList()); assertEquals(Collections.singletonList("Alice Liddell "), startsWithAlice); - List exactMatch = SelectUserId.or( + List exactMatch = userIds.stream().filter(SelectUserId.or( SelectUserId.exactMatch(""), SelectUserId.startsWith("Not Found") - ).selectUserIds(secretKeys); + )).collect(Collectors.toList()); assertEquals(Collections.singletonList(""), exactMatch); } @Test - public void testContainsSubstring() throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { + public void testContainsSubstring() throws PGPException { PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().simpleEcKeyRing("wine drinker"); secretKeys = PGPainless.modifyKeyRing(secretKeys) .addUserId("this is not a quine", SecretKeyRingProtector.unprotectedKeys()) .addUserId("this is not a crime", SecretKeyRingProtector.unprotectedKeys()) .done(); - List containSubstring = SelectUserId.containsSubstring("ine") - .selectUserIds(secretKeys); + List userIds = PGPainless.inspectKeyRing(secretKeys).getValidUserIds(); + + List containSubstring = userIds.stream().filter(SelectUserId.containsSubstring("ine")).collect(Collectors.toList()); assertEquals(Arrays.asList("wine drinker", "this is not a quine"), containSubstring); } @Test - public void testContainsEmailAddress() throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { + public void testContainsEmailAddress() { PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().simpleEcKeyRing("Alice "); + List userIds = PGPainless.inspectKeyRing(secretKeys).getValidUserIds(); - assertEquals("Alice ", SelectUserId.containsEmailAddress("alice@wonderland.lit").firstMatch(secretKeys)); - assertEquals("Alice ", SelectUserId.containsEmailAddress("").firstMatch(secretKeys)); + assertEquals("Alice ", userIds.stream().filter( + SelectUserId.containsEmailAddress("alice@wonderland.lit")).findFirst().get()); + assertEquals("Alice ", userIds.stream().filter( + SelectUserId.containsEmailAddress("")).findFirst().get()); - assertNull(SelectUserId.containsEmailAddress("mad@hatter.lit").firstMatch(secretKeys)); + assertFalse(userIds.stream().anyMatch(SelectUserId.containsEmailAddress("mad@hatter.lit"))); } @Test - public void testAndOrNot() throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { + public void testAndOrNot() throws PGPException { PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().simpleEcKeyRing("Alice "); secretKeys = PGPainless.modifyKeyRing(secretKeys) .addUserId("Alice ", SecretKeyRingProtector.unprotectedKeys()) @@ -82,33 +87,43 @@ public class SelectUserIdTest { .addUserId("Crazy Girl ", SecretKeyRingProtector.unprotectedKeys()) .done(); - List or = SelectUserId.or( + List userIds = PGPainless.inspectKeyRing(secretKeys).getValidUserIds(); + + List or = userIds.stream().filter(SelectUserId.or( SelectUserId.containsEmailAddress("alice@wonderland.lit"), - SelectUserId.startsWith("Alice")) - .selectUserIds(secretKeys); + SelectUserId.startsWith("Alice"))).collect(Collectors.toList()); assertEquals(Arrays.asList("Alice ", "Alice ", "Crazy Girl "), or); - List and = SelectUserId.and( + List and = userIds.stream().filter(SelectUserId.and( SelectUserId.containsEmailAddress("alice@wonderland.lit"), - SelectUserId.startsWith("Alice")) - .selectUserIds(secretKeys); + SelectUserId.startsWith("Alice"))).collect(Collectors.toList()); assertEquals(Collections.singletonList("Alice "), and); - List not = SelectUserId.not( - SelectUserId.startsWith("Alice")) - .selectUserIds(secretKeys); + List not = userIds.stream().filter(SelectUserId.not( + SelectUserId.startsWith("Alice"))).collect(Collectors.toList()); assertEquals(Arrays.asList("", "Crazy Girl "), not); } @Test - public void testFirstMatch() throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { + public void testFirstMatch() throws PGPException { PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().simpleEcKeyRing("First UserID"); secretKeys = PGPainless.modifyKeyRing(secretKeys) .addUserId("Second UserID", SecretKeyRingProtector.unprotectedKeys()) .done(); - assertEquals("First UserID", SelectUserId.validUserId(secretKeys).firstMatch(secretKeys)); - assertEquals("Second UserID", SelectUserId.containsSubstring("Second").firstMatch( - PGPainless.inspectKeyRing(secretKeys).getUserIds() - )); + List userIds = PGPainless.inspectKeyRing(secretKeys).getValidUserIds(); + assertEquals("First UserID", userIds.stream().filter(SelectUserId.validUserId(secretKeys)).findFirst().get()); + assertEquals("Second UserID", userIds.stream().filter(SelectUserId.containsSubstring("Second")).findFirst().get()); + } + + @Test + public void testByEmail() { + SelectUserId containsEmailAddress = SelectUserId.containsEmailAddress("alice@pgpainless.org"); + assertTrue(containsEmailAddress.accept("")); + assertTrue(containsEmailAddress.accept("Alice ")); + + SelectUserId byEmail = SelectUserId.byEmail("alice@pgpainless.org"); + assertTrue(byEmail.accept("alice@pgpainless.org")); + assertTrue(byEmail.accept("")); + assertTrue(byEmail.accept("Alice ")); } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/weird_keys/TestEncryptCommsStorageFlagsDifferentiated.java b/pgpainless-core/src/test/java/org/pgpainless/weird_keys/TestEncryptCommsStorageFlagsDifferentiated.java index d2540e73..b96d95e5 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/weird_keys/TestEncryptCommsStorageFlagsDifferentiated.java +++ b/pgpainless-core/src/test/java/org/pgpainless/weird_keys/TestEncryptCommsStorageFlagsDifferentiated.java @@ -6,7 +6,6 @@ package org.pgpainless.weird_keys; import static org.junit.jupiter.api.Assertions.assertThrows; -import java.io.IOException; import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; @@ -17,6 +16,7 @@ import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.KeyFlag; import org.pgpainless.encryption_signing.EncryptionOptions; +import org.pgpainless.exception.KeyException; import org.pgpainless.key.generation.KeySpec; import org.pgpainless.key.generation.type.KeyType; import org.pgpainless.key.generation.type.rsa.RsaLength; @@ -25,8 +25,9 @@ import org.pgpainless.key.util.KeyRingUtils; public class TestEncryptCommsStorageFlagsDifferentiated { @Test - public void testThatEncryptionDifferentiatesBetweenPurposeKeyFlags() throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException, IOException { - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() + public void testThatEncryptionDifferentiatesBetweenPurposeKeyFlags() + throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { + PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() .setPrimaryKey(KeySpec.getBuilder( KeyType.RSA(RsaLength._3072), KeyFlag.CERTIFY_OTHER, @@ -38,7 +39,7 @@ public class TestEncryptCommsStorageFlagsDifferentiated { PGPPublicKeyRing publicKeys = KeyRingUtils.publicKeyRingFrom(secretKeys); - assertThrows(IllegalArgumentException.class, () -> EncryptionOptions.encryptCommunications() + assertThrows(KeyException.UnacceptableEncryptionKeyException.class, () -> EncryptionOptions.encryptCommunications() .addRecipient(publicKeys)); } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/weird_keys/TestTwoSubkeysEncryption.java b/pgpainless-core/src/test/java/org/pgpainless/weird_keys/TestTwoSubkeysEncryption.java index dac83a7b..4b715a76 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/weird_keys/TestTwoSubkeysEncryption.java +++ b/pgpainless-core/src/test/java/org/pgpainless/weird_keys/TestTwoSubkeysEncryption.java @@ -51,7 +51,7 @@ public class TestTwoSubkeysEncryption { EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() .onOutputStream(out) .withOptions( - ProducerOptions.encrypt(new EncryptionOptions(EncryptionPurpose.STORAGE_AND_COMMUNICATIONS) + ProducerOptions.encrypt(new EncryptionOptions(EncryptionPurpose.ANY) .addRecipient(publicKeys, EncryptionOptions.encryptToAllCapableSubkeys()) ) .setAsciiArmor(false) diff --git a/pgpainless-core/src/test/kotlin/org/pgpainless/bouncycastle/extensions/PGPKeyRingExtensionsTest.kt b/pgpainless-core/src/test/kotlin/org/pgpainless/bouncycastle/extensions/PGPKeyRingExtensionsTest.kt new file mode 100644 index 00000000..49470c75 --- /dev/null +++ b/pgpainless-core/src/test/kotlin/org/pgpainless/bouncycastle/extensions/PGPKeyRingExtensionsTest.kt @@ -0,0 +1,56 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.bouncycastle.extensions + +import org.junit.jupiter.api.Assertions.assertDoesNotThrow +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.pgpainless.key.TestKeys + +class PGPKeyRingExtensionsTest { + + @Test + fun `public key ring has public key`() { + val key = TestKeys.getJulietPublicKeyRing() + assertTrue(key.hasPublicKey(TestKeys.JULIET_KEY_ID)) + assertTrue(key.hasPublicKey(TestKeys.JULIET_FINGERPRINT)) + + assertFalse(key.hasPublicKey(TestKeys.ROMEO_KEY_ID)) + assertFalse(key.hasPublicKey(TestKeys.ROMEO_FINGERPRINT)) + } + + @Test + fun `secret key ring has public key`() { + val key = TestKeys.getJulietSecretKeyRing() + assertTrue(key.hasPublicKey(TestKeys.JULIET_KEY_ID)) + assertTrue(key.hasPublicKey(TestKeys.JULIET_FINGERPRINT)) + + assertFalse(key.hasPublicKey(TestKeys.ROMEO_KEY_ID)) + assertFalse(key.hasPublicKey(TestKeys.ROMEO_FINGERPRINT)) + } + + @Test + fun `test requirePublicKey on secret key ring`() { + val key = TestKeys.getJulietSecretKeyRing() + assertNotNull(assertDoesNotThrow { key.requirePublicKey(TestKeys.JULIET_KEY_ID) }) + assertNotNull(assertDoesNotThrow { key.requirePublicKey(TestKeys.JULIET_FINGERPRINT) }) + + assertThrows { key.requirePublicKey(TestKeys.ROMEO_KEY_ID) } + assertThrows { key.requirePublicKey(TestKeys.ROMEO_FINGERPRINT) } + } + + @Test + fun `test requirePublicKey on public key ring`() { + val key = TestKeys.getJulietPublicKeyRing() + assertNotNull(assertDoesNotThrow { key.requirePublicKey(TestKeys.JULIET_KEY_ID) }) + assertNotNull(assertDoesNotThrow { key.requirePublicKey(TestKeys.JULIET_FINGERPRINT) }) + + assertThrows { key.requirePublicKey(TestKeys.ROMEO_KEY_ID) } + assertThrows { key.requirePublicKey(TestKeys.ROMEO_FINGERPRINT) } + } +} diff --git a/pgpainless-core/src/test/kotlin/org/pgpainless/bouncycastle/extensions/PGPPublicKeyExtensionsTest.kt b/pgpainless-core/src/test/kotlin/org/pgpainless/bouncycastle/extensions/PGPPublicKeyExtensionsTest.kt new file mode 100644 index 00000000..d9c99c47 --- /dev/null +++ b/pgpainless-core/src/test/kotlin/org/pgpainless/bouncycastle/extensions/PGPPublicKeyExtensionsTest.kt @@ -0,0 +1,60 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.bouncycastle.extensions + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.pgpainless.PGPainless +import org.pgpainless.algorithm.PublicKeyAlgorithm +import org.pgpainless.key.TestKeys +import org.pgpainless.key.generation.KeySpec +import org.pgpainless.key.generation.type.KeyType +import org.pgpainless.key.generation.type.ecc.EllipticCurve +import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve + +class PGPPublicKeyExtensionsTest { + + @Test + fun `test getCurveName for all ECDSA curves`() { + for (curve in EllipticCurve.values()) { + val key = + PGPainless.buildKeyRing() + .setPrimaryKey(KeySpec.getBuilder(KeyType.ECDSA(curve))) + .build() + .publicKey + + assertEquals(curve.curveName, key.getCurveName()) + } + } + + @Test + fun `test getCurveName for legacy EdDSA curves`() { + for (curve in EdDSALegacyCurve.values()) { + val key = + PGPainless.buildKeyRing() + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(curve))) + .build() + .publicKey + + assertEquals(curve.curveName, key.getCurveName()) + } + } + + @Test + fun `test getCurveName fails for non-curve keys`() { + // RSA + val key = TestKeys.getJulietPublicKeyRing() + assertEquals(PublicKeyAlgorithm.RSA_GENERAL, key.publicKey.publicKeyAlgorithm) + + assertThrows { key.publicKey.getCurveName() } + } + + @Test + fun `openPgpFingerprint returns fitting fingerprint`() { + val key = TestKeys.getEmilSecretKeyRing() + assertEquals(TestKeys.EMIL_FINGERPRINT, key.publicKey.openPgpFingerprint) + } +} diff --git a/pgpainless-core/src/test/kotlin/org/pgpainless/bouncycastle/extensions/PGPSecretKeyExtensionsTest.kt b/pgpainless-core/src/test/kotlin/org/pgpainless/bouncycastle/extensions/PGPSecretKeyExtensionsTest.kt new file mode 100644 index 00000000..6dc1e4d4 --- /dev/null +++ b/pgpainless-core/src/test/kotlin/org/pgpainless/bouncycastle/extensions/PGPSecretKeyExtensionsTest.kt @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.bouncycastle.extensions + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.assertThrows +import org.pgpainless.algorithm.PublicKeyAlgorithm +import org.pgpainless.exception.WrongPassphraseException +import org.pgpainless.key.TestKeys +import org.pgpainless.util.Passphrase + +class PGPSecretKeyExtensionsTest { + + @Test + fun `can successfully unlock protected secret key`() { + val key = TestKeys.getCryptieSecretKeyRing() + val secKey = key.secretKey + + val privKey = assertDoesNotThrow { secKey.unlock(TestKeys.CRYPTIE_PASSPHRASE) } + assertNotNull(privKey) + } + + @Test + fun `cannot unlock protected secret key using empty passphrase`() { + val key = TestKeys.getCryptieSecretKeyRing() + val secKey = key.secretKey + + assertThrows { secKey.unlock(Passphrase.emptyPassphrase()) } + } + + @Test + fun `can successfully unlock unprotected secret key with unprotectedKeys protector`() { + val key = TestKeys.getEmilSecretKeyRing() + val secKey = key.secretKey + + val privKey = assertDoesNotThrow { secKey.unlock() } + assertNotNull(privKey) + } + + @Test + fun `can successfully unlock unprotected secret key with empty passphrase`() { + val key = TestKeys.getEmilSecretKeyRing() + val secKey = key.secretKey + + val privKey = assertDoesNotThrow { secKey.unlock(Passphrase.emptyPassphrase()) } + assertNotNull(privKey) + } + + @Test + fun `openPgpFingerprint returns fitting fingerprint`() { + val key = TestKeys.getEmilSecretKeyRing() + + assertEquals(TestKeys.EMIL_FINGERPRINT, key.openPgpFingerprint) + assertEquals(TestKeys.EMIL_FINGERPRINT, key.secretKey.openPgpFingerprint) + } + + @Test + fun `publicKeyAlgorithm returns fitting algorithm`() { + val key = TestKeys.getEmilSecretKeyRing() + assertEquals(PublicKeyAlgorithm.ECDSA, key.secretKey.publicKeyAlgorithm) + } +} diff --git a/pgpainless-core/src/test/kotlin/org/pgpainless/bouncycastle/extensions/PGPSecretKeyRingExtensionsTest.kt b/pgpainless-core/src/test/kotlin/org/pgpainless/bouncycastle/extensions/PGPSecretKeyRingExtensionsTest.kt new file mode 100644 index 00000000..90641aff --- /dev/null +++ b/pgpainless-core/src/test/kotlin/org/pgpainless/bouncycastle/extensions/PGPSecretKeyRingExtensionsTest.kt @@ -0,0 +1,59 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.bouncycastle.extensions + +import java.io.ByteArrayOutputStream +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.pgpainless.PGPainless +import org.pgpainless.encryption_signing.ProducerOptions +import org.pgpainless.encryption_signing.SigningOptions +import org.pgpainless.key.TestKeys +import org.pgpainless.key.protection.SecretKeyRingProtector + +class PGPSecretKeyRingExtensionsTest { + + @Test + fun testHasPgpSecretKeyRing() { + val key = TestKeys.getEmilSecretKeyRing() + assertTrue(key.hasSecretKey(TestKeys.EMIL_KEY_ID)) + assertTrue(key.hasSecretKey(TestKeys.EMIL_FINGERPRINT)) + + assertFalse(key.hasSecretKey(TestKeys.ROMEO_KEY_ID)) + assertFalse(key.hasSecretKey(TestKeys.ROMEO_FINGERPRINT)) + } + + @Test + fun testRequireSecretKey() { + val key = TestKeys.getEmilSecretKeyRing() + assertNotNull(key.requireSecretKey(TestKeys.EMIL_KEY_ID)) + assertNotNull(key.requireSecretKey(TestKeys.EMIL_FINGERPRINT)) + + assertThrows { key.requireSecretKey(TestKeys.ROMEO_KEY_ID) } + assertThrows { key.requireSecretKey(TestKeys.ROMEO_FINGERPRINT) } + } + + @Test + fun testGetSecretKeyForSignature() { + val key = TestKeys.getEmilSecretKeyRing() + val signer = + PGPainless.encryptAndOrSign() + .onOutputStream(ByteArrayOutputStream()) + .withOptions( + ProducerOptions.sign( + SigningOptions.get() + .addDetachedSignature(SecretKeyRingProtector.unprotectedKeys(), key))) + signer.write("Hello, World!\n".toByteArray()) + signer.close() + val sig = signer.result.detachedSignatures.first().value.first() + + assertNotNull(key.getSecretKeyFor(sig)) + assertNull(TestKeys.getRomeoSecretKeyRing().getSecretKeyFor(sig)) + } +} diff --git a/pgpainless-core/src/test/kotlin/org/pgpainless/key/KeyWithUnknownSecretKeyEncryptionMethodTest.kt b/pgpainless-core/src/test/kotlin/org/pgpainless/key/KeyWithUnknownSecretKeyEncryptionMethodTest.kt new file mode 100644 index 00000000..253c0f19 --- /dev/null +++ b/pgpainless-core/src/test/kotlin/org/pgpainless/key/KeyWithUnknownSecretKeyEncryptionMethodTest.kt @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Test +import org.pgpainless.PGPainless + +class KeyWithUnknownSecretKeyEncryptionMethodTest { + + // Test vector from https://gitlab.com/dkg/openpgp-hardware-secrets/-/merge_requests/2 + val KEY = + """-----BEGIN PGP PRIVATE KEY BLOCK----- + +xTQEZgWtcxYJKwYBBAHaRw8BAQdAlLK6UPQsVHR2ETk1SwVIG3tBmpiEtikYYlCy +1TIiqzb8zR08aGFyZHdhcmUtc2VjcmV0QGV4YW1wbGUub3JnPsKNBBAWCAA1AhkB +BQJmBa1zAhsDCAsJCAcKDQwLBRUKCQgLAhYCFiEEXlP8Tur0WZR+f0I33/i9Uh4O +HEkACgkQ3/i9Uh4OHEnryAD8CzH2ajJvASp46ApfI4pLPY57rjBX++d/2FQPRyqG +HJUA/RLsNNgxiFYmK5cjtQe2/DgzWQ7R6PxPC6oa3XM7xPcCxzkEZgWtcxIKKwYB +BAGXVQEFAQEHQE1YXOKeaklwG01Yab4xopP9wbu1E+pCrP1xQpiFZW5KAwEIB/zC +eAQYFggAIAUCZgWtcwIbDBYhBF5T/E7q9FmUfn9CN9/4vVIeDhxJAAoJEN/4vVIe +DhxJVTgA/1WaFrKdP3AgL0Ffdooc5XXbjQsj0uHo6FZSHRI4pchMAQCyJnKQ3RvW +/0gm41JCqImyg2fxWG4hY0N5Q7Rc6PyzDQ== +=3w/O +-----END PGP PRIVATE KEY BLOCK-----""" + + @Test + @Disabled("Disabled since BC 1.77 chokes on the test key") + fun testExtractCertificate() { + val key = PGPainless.readKeyRing().secretKeyRing(KEY)!! + val cert = PGPainless.extractCertificate(key) + + assertNotNull(cert) + // Each secret key got its public key component extracted + assertEquals( + key.secretKeys.asSequence().map { it.keyID }.toSet(), + cert.publicKeys.asSequence().map { it.keyID }.toSet()) + } +} diff --git a/pgpainless-core/src/test/kotlin/org/pgpainless/key/KeyWithUnsupportedSignatureSubpacketTest.kt b/pgpainless-core/src/test/kotlin/org/pgpainless/key/KeyWithUnsupportedSignatureSubpacketTest.kt new file mode 100644 index 00000000..e557adc3 --- /dev/null +++ b/pgpainless-core/src/test/kotlin/org/pgpainless/key/KeyWithUnsupportedSignatureSubpacketTest.kt @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key + +import java.util.* +import org.bouncycastle.openpgp.PGPSecretKeyRing +import org.junit.jupiter.api.Test +import org.pgpainless.PGPainless +import org.pgpainless.key.protection.SecretKeyRingProtector + +class KeyWithUnsupportedSignatureSubpacketTest { + + @Test + fun `can set new expiration date on key containing unknown subpacket 34`() { + val armoredKey = + """-----BEGIN PGP PRIVATE KEY BLOCK----- + +lFgEZWiyNhYJKwYBBAHaRw8BAQdA71QipJ0CAqOEqQWjuoQE4E7LarKSrNDwE/6K +bQNrCLwAAQCtJ8kVG2AmbDfdVtr/7Ag+yBh0oCvjRvyUCOyIbruOeg+6tClTdWJw +YWNrZXQzNCBUZXN0S2V5IDx0ZXN0QHBncGFpbmxlc3Mub3JnPoiTBBMWCgA7FiEE +zhy5yrnZYU/iBza4G03SQVuWqx0FAmVosjYCGwMFCwkIBwICIgIGFQoJCAsCBBYC +AwECHgcCF4AACgkQG03SQVuWqx1UGgD+IYLeh9t5eJCEnzueuOTYnTnrzyhnLgm9 +dw5qwMXU8VQA/28GCOb7610hyjiBbrrcshkWAKuMwp8bUSz5FOeS5cQEnF0EZWiy +NhIKKwYBBAGXVQEFAQEHQK99ClLDYtn0I2b6Y26NhaL0RWcrNoI/ci0xgXEK2L0Y +AwEIBwAA/06qciQHI0v7MP2LMWm/ZuTJwzlPqV8VsBhrDMyUPUD4D52IeAQYFgoA +IBYhBM4cucq52WFP4gc2uBtN0kFblqsdBQJlaLI2AhsMAAoJEBtN0kFblqsdRQ0A +/iUJ/Fp+D2RjZL+aiwByIxPCVvMJ7a28+GQGjg3hsU2BAP474dfOOVZiTDLWWxsB +wxfzOAQxXDhgR9xd/Lk3MNJxDg== +=YAt0 +-----END PGP PRIVATE KEY BLOCK-----""" + val key: PGPSecretKeyRing = PGPainless.readKeyRing().secretKeyRing(armoredKey)!! + PGPainless.modifyKeyRing(secretKey = key) + .setExpirationDate(Date(), SecretKeyRingProtector.unprotectedKeys()) + .done() + } +} diff --git a/pgpainless-core/src/test/resources/logback-test.xml b/pgpainless-core/src/test/resources/logback-test.xml new file mode 100644 index 00000000..bb16e293 --- /dev/null +++ b/pgpainless-core/src/test/resources/logback-test.xml @@ -0,0 +1,29 @@ + + + + + System.err + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + System.out + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + \ No newline at end of file diff --git a/pgpainless-sop/README.md b/pgpainless-sop/README.md index 15aa483c..7f7267cd 100644 --- a/pgpainless-sop/README.md +++ b/pgpainless-sop/README.md @@ -6,7 +6,73 @@ SPDX-License-Identifier: Apache-2.0 # PGPainless-SOP +[![Spec Revision: 10](https://img.shields.io/badge/Spec%20Revision-10-blue)](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/) +[![Maven Central](https://badgen.net/maven/v/maven-central/org.pgpainless/pgpainless-sop)](https://search.maven.org/artifact/org.pgpainless/pgpainless-sop) +[![javadoc](https://javadoc.io/badge2/org.pgpainless/pgpainless-sop/javadoc.svg)](https://javadoc.io/doc/org.pgpainless/pgpainless-sop) + Implementation of the Stateless OpenPGP Protocol using PGPainless. This module implements `sop-java` using `pgpainless-core`. -If your code depends on `sop-java`, this module can be used as a realization of those interfaces. \ No newline at end of file +If your code depends on `sop-java`, this module can be used as a realization of those interfaces. + +## Get started + +To start using pgpainless-sop in your code, include the following lines in your build script: +``` +// If you use Gradle +... +dependencies { + ... + implementation "org.pgpainless:pgpainless-sop:1.7.6" + ... +} + +// If you use Maven +... + + ... + + org.pgpainless + pgpainless-sop + 1.7.6 + + ... + +``` + +`pgpainless-sop` will transitively pull in its dependencies, such as `sop-java` and `pgpainless-core`. + +## Usage Examples +```java +SOP sop = new SOPImpl(); + +// Generate an OpenPGP key +byte[] key = sop.generateKey() + .userId("Alice ") + .generate() + .getBytes(); + +// Extract the certificate (public key) +byte[] cert = sop.extractCert() + .key(key) + .getBytes(); + +// Encrypt a message +byte[] message = ... +byte[] encrypted = sop.encrypt() + .withCert(cert) + .signWith(key) + .plaintext(message) + .getBytes(); + +// Decrypt a message +ByteArrayAndResult messageAndVerifications = sop.decrypt() + .verifyWithCert(cert) + .withKey(key) + .ciphertext(encrypted) + .toByteArrayAndResult(); +byte[] decrypted = messageAndVerifications.getBytes(); +// Signature Verifications +DecryptionResult messageInfo = messageAndVerifications.getResult(); +List signatureVerifications = messageInfo.getVerifications(); +``` diff --git a/pgpainless-sop/build.gradle b/pgpainless-sop/build.gradle index 4858dbab..53b499c6 100644 --- a/pgpainless-sop/build.gradle +++ b/pgpainless-sop/build.gradle @@ -1,27 +1,49 @@ // SPDX-FileCopyrightText: 2021 Paul Schaub // // SPDX-License-Identifier: Apache-2.0 - +import org.apache.tools.ant.filters.* plugins { - id 'java' + id 'java-library' } group 'org.pgpainless' repositories { mavenCentral() + mavenLocal() } dependencies { - testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.0' - testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.0' + testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion" + testImplementation "org.junit.jupiter:junit-jupiter-params:$junitVersion" + testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion" - testImplementation 'ch.qos.logback:logback-classic:1.2.5' + // Logging + testImplementation "ch.qos.logback:logback-classic:$logbackVersion" + + // Depend on "shared" sop-java test suite (fixtures are turned into tests by inheritance inside test sources) + testImplementation "org.pgpainless:sop-java-testfixtures:$sopJavaVersion" implementation(project(":pgpainless-core")) - implementation(project(":sop-java")) + api "org.pgpainless:sop-java:$sopJavaVersion" + + implementation "com.google.code.findbugs:jsr305:3.0.2" +} + +processResources { + filter ReplaceTokens, tokens: [ + "project.version": project.version.toString() + ] } test { useJUnitPlatform() + environment("test.implementation", "sop.testsuite.pgpainless.PGPainlessSopInstanceFactory") +} + +// https://docs.gradle.org/current/userguide/java_library_plugin.html#sec:java_library_modular_auto +tasks.named('jar') { + manifest { + attributes('Automatic-Module-Name': 'org.pgpainless.sop') + } } diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/ArmorImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/ArmorImpl.java deleted file mode 100644 index 5c92f9b9..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/ArmorImpl.java +++ /dev/null @@ -1,37 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.sop; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -import org.bouncycastle.bcpg.ArmoredOutputStream; -import org.bouncycastle.util.io.Streams; -import org.pgpainless.util.ArmoredOutputStreamFactory; -import sop.Ready; -import sop.enums.ArmorLabel; -import sop.exception.SOPGPException; -import sop.operation.Armor; - -public class ArmorImpl implements Armor { - - @Override - public Armor label(ArmorLabel label) throws SOPGPException.UnsupportedOption { - throw new SOPGPException.UnsupportedOption("Setting custom Armor labels not supported."); - } - - @Override - public Ready data(InputStream data) throws SOPGPException.BadData { - return new Ready() { - @Override - public void writeTo(OutputStream outputStream) throws IOException { - ArmoredOutputStream armor = ArmoredOutputStreamFactory.get(outputStream); - Streams.pipeAll(data, armor); - armor.close(); - } - }; - } -} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DearmorImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DearmorImpl.java deleted file mode 100644 index 73bd65d5..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DearmorImpl.java +++ /dev/null @@ -1,30 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.sop; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -import org.bouncycastle.openpgp.PGPUtil; -import org.bouncycastle.util.io.Streams; -import sop.Ready; -import sop.operation.Dearmor; - -public class DearmorImpl implements Dearmor { - - @Override - public Ready data(InputStream data) throws IOException { - InputStream decoder = PGPUtil.getDecoderStream(data); - return new Ready() { - - @Override - public void writeTo(OutputStream outputStream) throws IOException { - Streams.pipeAll(decoder, outputStream); - decoder.close(); - } - }; - } -} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java deleted file mode 100644 index 7298065e..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java +++ /dev/null @@ -1,160 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.sop; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; -import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.util.io.Streams; -import org.pgpainless.PGPainless; -import org.pgpainless.decryption_verification.ConsumerOptions; -import org.pgpainless.decryption_verification.DecryptionStream; -import org.pgpainless.decryption_verification.OpenPgpMetadata; -import org.pgpainless.key.SubkeyIdentifier; -import org.pgpainless.key.info.KeyRingInfo; -import org.pgpainless.key.protection.SecretKeyRingProtector; -import org.pgpainless.util.Passphrase; -import sop.DecryptionResult; -import sop.ReadyWithResult; -import sop.SessionKey; -import sop.Verification; -import sop.exception.SOPGPException; -import sop.operation.Decrypt; - -public class DecryptImpl implements Decrypt { - - private final ConsumerOptions consumerOptions = new ConsumerOptions(); - - @Override - public DecryptImpl verifyNotBefore(Date timestamp) throws SOPGPException.UnsupportedOption { - consumerOptions.verifyNotBefore(timestamp); - return this; - } - - @Override - public DecryptImpl verifyNotAfter(Date timestamp) throws SOPGPException.UnsupportedOption { - consumerOptions.verifyNotAfter(timestamp); - return this; - } - - @Override - public DecryptImpl verifyWithCert(InputStream certIn) throws SOPGPException.BadData, IOException { - try { - PGPPublicKeyRingCollection certs = PGPainless.readKeyRing().keyRingCollection(certIn, false) - .getPgpPublicKeyRingCollection(); - if (certs.size() == 0) { - throw new SOPGPException.BadData(new PGPException("No certificates provided.")); - } - - consumerOptions.addVerificationCerts(certs); - - } catch (PGPException e) { - throw new SOPGPException.BadData(e); - } - return this; - } - - @Override - public DecryptImpl withSessionKey(SessionKey sessionKey) throws SOPGPException.UnsupportedOption { - throw new SOPGPException.UnsupportedOption("Setting custom session key not supported."); - } - - @Override - public DecryptImpl withPassword(String password) { - consumerOptions.addDecryptionPassphrase(Passphrase.fromPassword(password)); - String withoutTrailingWhitespace = removeTrailingWhitespace(password); - if (!password.equals(withoutTrailingWhitespace)) { - consumerOptions.addDecryptionPassphrase(Passphrase.fromPassword(withoutTrailingWhitespace)); - } - return this; - } - - private static String removeTrailingWhitespace(String passphrase) { - int i = passphrase.length() - 1; - // Find index of first non-whitespace character from the back - while (i > 0 && Character.isWhitespace(passphrase.charAt(i))) { - i--; - } - return passphrase.substring(0, i); - } - - @Override - public DecryptImpl withKey(InputStream keyIn) throws SOPGPException.KeyIsProtected, SOPGPException.BadData, SOPGPException.UnsupportedAsymmetricAlgo { - try { - PGPSecretKeyRingCollection secretKeys = PGPainless.readKeyRing() - .secretKeyRingCollection(keyIn); - - if (secretKeys.size() != 1) { - throw new SOPGPException.BadData(new AssertionError("Exactly one single secret key expected. Got " + secretKeys.size())); - } - - for (PGPSecretKeyRing secretKey : secretKeys) { - KeyRingInfo info = new KeyRingInfo(secretKey); - if (!info.isFullyDecrypted()) { - throw new SOPGPException.KeyIsProtected(); - } - } - - consumerOptions.addDecryptionKeys(secretKeys, SecretKeyRingProtector.unprotectedKeys()); - } catch (IOException | PGPException e) { - throw new SOPGPException.BadData(e); - } - return this; - } - - @Override - public ReadyWithResult ciphertext(InputStream ciphertext) - throws SOPGPException.BadData, - SOPGPException.MissingArg { - - if (consumerOptions.getDecryptionKeys().isEmpty() && consumerOptions.getDecryptionPassphrases().isEmpty()) { - throw new SOPGPException.MissingArg("Missing decryption key or passphrase."); - } - - DecryptionStream decryptionStream; - try { - decryptionStream = PGPainless.decryptAndOrVerify() - .onInputStream(ciphertext) - .withOptions(consumerOptions); - } catch (PGPException | IOException e) { - throw new SOPGPException.BadData(e); - } - - return new ReadyWithResult() { - @Override - public DecryptionResult writeTo(OutputStream outputStream) throws IOException, SOPGPException.NoSignature { - Streams.pipeAll(decryptionStream, outputStream); - decryptionStream.close(); - OpenPgpMetadata metadata = decryptionStream.getResult(); - - List verificationList = new ArrayList<>(); - for (SubkeyIdentifier verifiedSigningKey : metadata.getVerifiedSignatures().keySet()) { - PGPSignature signature = metadata.getVerifiedSignatures().get(verifiedSigningKey); - verificationList.add(new Verification( - signature.getCreationTime(), - verifiedSigningKey.getSubkeyFingerprint().toString(), - verifiedSigningKey.getPrimaryKeyFingerprint().toString())); - } - - if (!consumerOptions.getCertificates().isEmpty()) { - if (verificationList.isEmpty()) { - throw new SOPGPException.NoSignature(); - } - } - - return new DecryptionResult(null, verificationList); - } - }; - } -} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachInbandSignatureAndMessageImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachInbandSignatureAndMessageImpl.java deleted file mode 100644 index b1558a90..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachInbandSignatureAndMessageImpl.java +++ /dev/null @@ -1,72 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.sop; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -import org.bouncycastle.bcpg.ArmoredOutputStream; -import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.PGPSignatureList; -import org.bouncycastle.util.io.Streams; -import org.pgpainless.exception.WrongConsumingMethodException; -import org.pgpainless.decryption_verification.cleartext_signatures.ClearsignedMessageUtil; -import org.pgpainless.util.ArmoredOutputStreamFactory; -import sop.ReadyWithResult; -import sop.Signatures; -import sop.exception.SOPGPException; -import sop.operation.DetachInbandSignatureAndMessage; - -public class DetachInbandSignatureAndMessageImpl implements DetachInbandSignatureAndMessage { - - private boolean armor = true; - - @Override - public DetachInbandSignatureAndMessage noArmor() { - this.armor = false; - return this; - } - - @Override - public ReadyWithResult message(InputStream messageInputStream) { - - return new ReadyWithResult() { - - private ByteArrayOutputStream sigOut = new ByteArrayOutputStream(); - @Override - public Signatures writeTo(OutputStream messageOutputStream) throws SOPGPException.NoSignature, IOException { - - PGPSignatureList signatures = null; - try { - signatures = ClearsignedMessageUtil.detachSignaturesFromInbandClearsignedMessage(messageInputStream, messageOutputStream); - } catch (WrongConsumingMethodException e) { - throw new IOException(e); - } - - if (armor) { - ArmoredOutputStream armorOut = ArmoredOutputStreamFactory.get(sigOut); - for (PGPSignature signature : signatures) { - signature.encode(armorOut); - } - armorOut.close(); - } else { - for (PGPSignature signature : signatures) { - signature.encode(sigOut); - } - } - - return new Signatures() { - @Override - public void writeTo(OutputStream signatureOutputStream) throws IOException { - Streams.pipeAll(new ByteArrayInputStream(sigOut.toByteArray()), signatureOutputStream); - } - }; - } - }; - } -} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java deleted file mode 100644 index b869d6ba..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java +++ /dev/null @@ -1,132 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.sop; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; -import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; -import org.bouncycastle.util.io.Streams; -import org.pgpainless.PGPainless; -import org.pgpainless.algorithm.DocumentSignatureType; -import org.pgpainless.algorithm.StreamEncoding; -import org.pgpainless.encryption_signing.EncryptionOptions; -import org.pgpainless.encryption_signing.EncryptionStream; -import org.pgpainless.encryption_signing.ProducerOptions; -import org.pgpainless.encryption_signing.SigningOptions; -import org.pgpainless.exception.WrongPassphraseException; -import org.pgpainless.key.protection.SecretKeyRingProtector; -import org.pgpainless.util.Passphrase; -import sop.util.ProxyOutputStream; -import sop.Ready; -import sop.enums.EncryptAs; -import sop.exception.SOPGPException; -import sop.operation.Encrypt; - -public class EncryptImpl implements Encrypt { - - EncryptionOptions encryptionOptions = new EncryptionOptions(); - SigningOptions signingOptions = null; - - private EncryptAs encryptAs = EncryptAs.Binary; - boolean armor = true; - - @Override - public Encrypt noArmor() { - armor = false; - return this; - } - - @Override - public Encrypt mode(EncryptAs mode) throws SOPGPException.UnsupportedOption { - this.encryptAs = mode; - return this; - } - - @Override - public Encrypt signWith(InputStream keyIn) throws SOPGPException.KeyIsProtected, SOPGPException.CertCannotSign, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData { - try { - PGPSecretKeyRingCollection keys = PGPainless.readKeyRing().secretKeyRingCollection(keyIn); - if (keys.size() != 1) { - throw new SOPGPException.BadData(new AssertionError("Exactly one secret key at a time expected. Got " + keys.size())); - } - - if (signingOptions == null) { - signingOptions = SigningOptions.get(); - } - try { - signingOptions.addInlineSignatures(SecretKeyRingProtector.unprotectedKeys(), keys, DocumentSignatureType.BINARY_DOCUMENT); - } catch (IllegalArgumentException e) { - throw new SOPGPException.CertCannotSign(); - } catch (WrongPassphraseException e) { - throw new SOPGPException.KeyIsProtected(); - } - } catch (IOException | PGPException e) { - throw new SOPGPException.BadData(e); - } - return this; - } - - @Override - public Encrypt withPassword(String password) throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption { - encryptionOptions.addPassphrase(Passphrase.fromPassword(password)); - return this; - } - - @Override - public Encrypt withCert(InputStream cert) throws SOPGPException.CertCannotEncrypt, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData { - try { - PGPPublicKeyRingCollection certificates = PGPainless.readKeyRing() - .keyRingCollection(cert, false) - .getPgpPublicKeyRingCollection(); - encryptionOptions.addRecipients(certificates); - } catch (IOException | PGPException e) { - throw new SOPGPException.BadData(e); - } - return this; - } - - @Override - public Ready plaintext(InputStream plaintext) throws IOException { - ProducerOptions producerOptions = signingOptions != null ? - ProducerOptions.signAndEncrypt(encryptionOptions, signingOptions) : - ProducerOptions.encrypt(encryptionOptions); - producerOptions.setAsciiArmor(armor); - producerOptions.setEncoding(encryptAsToStreamEncoding(encryptAs)); - - try { - ProxyOutputStream proxy = new ProxyOutputStream(); - EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() - .onOutputStream(proxy) - .withOptions(producerOptions); - - return new Ready() { - @Override - public void writeTo(OutputStream outputStream) throws IOException { - proxy.replaceOutputStream(outputStream); - Streams.pipeAll(plaintext, encryptionStream); - encryptionStream.close(); - } - }; - } catch (PGPException e) { - throw new IOException(); - } - } - - private static StreamEncoding encryptAsToStreamEncoding(EncryptAs encryptAs) { - switch (encryptAs) { - case Binary: - return StreamEncoding.BINARY; - case Text: - return StreamEncoding.TEXT; - case MIME: - return StreamEncoding.UTF8; - } - throw new IllegalArgumentException("Invalid value encountered: " + encryptAs); - } -} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/ExtractCertImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/ExtractCertImpl.java deleted file mode 100644 index d22c71c3..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/ExtractCertImpl.java +++ /dev/null @@ -1,52 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.sop; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.pgpainless.PGPainless; -import org.pgpainless.key.util.KeyRingUtils; -import org.pgpainless.util.ArmorUtils; -import sop.operation.ExtractCert; -import sop.Ready; -import sop.exception.SOPGPException; - -public class ExtractCertImpl implements ExtractCert { - - private boolean armor = true; - - @Override - public ExtractCert noArmor() { - armor = false; - return this; - } - - @Override - public Ready key(InputStream keyInputStream) throws IOException, SOPGPException.BadData { - PGPSecretKeyRing key = PGPainless.readKeyRing().secretKeyRing(keyInputStream); - if (key == null) { - throw new SOPGPException.BadData(new PGPException("No key data found.")); - } - - PGPPublicKeyRing cert = KeyRingUtils.publicKeyRingFrom(key); - - return new Ready() { - @Override - public void writeTo(OutputStream outputStream) throws IOException { - OutputStream out = armor ? ArmorUtils.createArmoredOutputStreamFor(cert, outputStream) : outputStream; - cert.encode(out); - - if (armor) { - out.close(); - } - } - }; - } -} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java deleted file mode 100644 index e79475ce..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java +++ /dev/null @@ -1,84 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.sop; - -import java.io.IOException; -import java.io.OutputStream; -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; -import java.util.Iterator; -import java.util.LinkedHashSet; -import java.util.Set; - -import org.bouncycastle.bcpg.ArmoredOutputStream; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.pgpainless.PGPainless; -import org.pgpainless.key.modification.secretkeyring.SecretKeyRingEditorInterface; -import org.pgpainless.key.protection.SecretKeyRingProtector; -import org.pgpainless.util.ArmorUtils; -import sop.Ready; -import sop.exception.SOPGPException; -import sop.operation.GenerateKey; - -public class GenerateKeyImpl implements GenerateKey { - - private boolean armor = true; - private final Set userIds = new LinkedHashSet<>(); - - @Override - public GenerateKey noArmor() { - this.armor = false; - return this; - } - - @Override - public GenerateKey userId(String userId) { - this.userIds.add(userId); - return this; - } - - @Override - public Ready generate() throws SOPGPException.MissingArg, SOPGPException.UnsupportedAsymmetricAlgo, IOException { - Iterator userIdIterator = userIds.iterator(); - if (!userIdIterator.hasNext()) { - throw new SOPGPException.MissingArg("Missing user-id."); - } - - PGPSecretKeyRing key; - try { - key = PGPainless.generateKeyRing() - .modernKeyRing(userIdIterator.next(), null); - - if (userIdIterator.hasNext()) { - SecretKeyRingEditorInterface editor = PGPainless.modifyKeyRing(key); - - while (userIdIterator.hasNext()) { - editor.addUserId(userIdIterator.next(), SecretKeyRingProtector.unprotectedKeys()); - } - - key = editor.done(); - } - - PGPSecretKeyRing finalKey = key; - return new Ready() { - @Override - public void writeTo(OutputStream outputStream) throws IOException { - if (armor) { - ArmoredOutputStream armoredOutputStream = ArmorUtils.toAsciiArmoredStream(finalKey, outputStream); - finalKey.encode(armoredOutputStream); - armoredOutputStream.close(); - } else { - finalKey.encode(outputStream); - } - } - }; - } catch (InvalidAlgorithmParameterException | NoSuchAlgorithmException e) { - throw new SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", e); - } catch (PGPException e) { - throw new RuntimeException(e); - } - } -} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java deleted file mode 100644 index cfa426a5..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java +++ /dev/null @@ -1,70 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.sop; - -import sop.SOP; -import sop.operation.Armor; -import sop.operation.Dearmor; -import sop.operation.Decrypt; -import sop.operation.DetachInbandSignatureAndMessage; -import sop.operation.Encrypt; -import sop.operation.ExtractCert; -import sop.operation.GenerateKey; -import sop.operation.Sign; -import sop.operation.Verify; -import sop.operation.Version; - -public class SOPImpl implements SOP { - - @Override - public Version version() { - return new VersionImpl(); - } - - @Override - public GenerateKey generateKey() { - return new GenerateKeyImpl(); - } - - @Override - public ExtractCert extractCert() { - return new ExtractCertImpl(); - } - - @Override - public Sign sign() { - return new SignImpl(); - } - - @Override - public Verify verify() { - return new VerifyImpl(); - } - - @Override - public Encrypt encrypt() { - return new EncryptImpl(); - } - - @Override - public Decrypt decrypt() { - return new DecryptImpl(); - } - - @Override - public Armor armor() { - return new ArmorImpl(); - } - - @Override - public Dearmor dearmor() { - return new DearmorImpl(); - } - - @Override - public DetachInbandSignatureAndMessage detachInbandSignatureAndMessage() { - return new DetachInbandSignatureAndMessageImpl(); - } -} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/SignImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/SignImpl.java deleted file mode 100644 index 22b6ed32..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/SignImpl.java +++ /dev/null @@ -1,122 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.sop; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.List; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; -import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.util.io.Streams; -import org.pgpainless.PGPainless; -import org.pgpainless.algorithm.DocumentSignatureType; -import org.pgpainless.encryption_signing.EncryptionResult; -import org.pgpainless.encryption_signing.EncryptionStream; -import org.pgpainless.encryption_signing.ProducerOptions; -import org.pgpainless.encryption_signing.SigningOptions; -import org.pgpainless.key.SubkeyIdentifier; -import org.pgpainless.key.info.KeyRingInfo; -import org.pgpainless.key.protection.SecretKeyRingProtector; -import org.pgpainless.util.ArmoredOutputStreamFactory; -import sop.Ready; -import sop.enums.SignAs; -import sop.exception.SOPGPException; -import sop.operation.Sign; - -public class SignImpl implements Sign { - - private boolean armor = true; - private SignAs mode = SignAs.Binary; - private final SigningOptions signingOptions = new SigningOptions(); - - @Override - public Sign noArmor() { - armor = false; - return this; - } - - @Override - public Sign mode(SignAs mode) { - this.mode = mode; - return this; - } - - @Override - public Sign key(InputStream keyIn) throws SOPGPException.KeyIsProtected, SOPGPException.BadData, IOException { - try { - PGPSecretKeyRingCollection keys = PGPainless.readKeyRing().secretKeyRingCollection(keyIn); - if (keys.size() != 1) { - throw new SOPGPException.BadData(new AssertionError("Exactly one secret key at a time expected. Got " + keys.size())); - } - - PGPSecretKeyRing key = keys.iterator().next(); - KeyRingInfo info = new KeyRingInfo(key); - if (!info.isFullyDecrypted()) { - throw new SOPGPException.KeyIsProtected(); - } - signingOptions.addDetachedSignature(SecretKeyRingProtector.unprotectedKeys(), key, modeToSigType(mode)); - } catch (PGPException e) { - throw new SOPGPException.BadData(e); - } - return this; - } - - @Override - public Ready data(InputStream data) throws IOException { - ByteArrayOutputStream buffer = new ByteArrayOutputStream(); - try { - EncryptionStream signingStream = PGPainless.encryptAndOrSign() - .onOutputStream(buffer) - .withOptions(ProducerOptions.sign(signingOptions) - .setAsciiArmor(armor)); - - return new Ready() { - @Override - public void writeTo(OutputStream outputStream) throws IOException { - - if (signingStream.isClosed()) { - throw new IllegalStateException("EncryptionStream is already closed."); - } - - Streams.pipeAll(data, signingStream); - signingStream.close(); - EncryptionResult encryptionResult = signingStream.getResult(); - - List signatures = new ArrayList<>(); - for (SubkeyIdentifier key : encryptionResult.getDetachedSignatures().keySet()) { - signatures.addAll(encryptionResult.getDetachedSignatures().get(key)); - } - - OutputStream out; - if (armor) { - out = ArmoredOutputStreamFactory.get(outputStream); - } else { - out = outputStream; - } - for (PGPSignature sig : signatures) { - sig.encode(out); - } - out.close(); - outputStream.close(); // armor out does not close underlying stream - } - }; - - } catch (PGPException e) { - throw new RuntimeException(e); - } - - } - - private static DocumentSignatureType modeToSigType(SignAs mode) { - return mode == SignAs.Binary ? DocumentSignatureType.BINARY_DOCUMENT - : DocumentSignatureType.CANONICAL_TEXT_DOCUMENT; - } -} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/VerifyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/VerifyImpl.java deleted file mode 100644 index cdfa465c..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/VerifyImpl.java +++ /dev/null @@ -1,97 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.sop; - -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; -import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.util.io.Streams; -import org.pgpainless.PGPainless; -import org.pgpainless.decryption_verification.ConsumerOptions; -import org.pgpainless.decryption_verification.DecryptionStream; -import org.pgpainless.decryption_verification.OpenPgpMetadata; -import org.pgpainless.key.SubkeyIdentifier; -import sop.Verification; -import sop.exception.SOPGPException; -import sop.operation.Verify; - -public class VerifyImpl implements Verify { - - ConsumerOptions options = new ConsumerOptions(); - - @Override - public Verify notBefore(Date timestamp) throws SOPGPException.UnsupportedOption { - options.verifyNotBefore(timestamp); - return this; - } - - @Override - public Verify notAfter(Date timestamp) throws SOPGPException.UnsupportedOption { - options.verifyNotAfter(timestamp); - return this; - } - - @Override - public Verify cert(InputStream cert) throws SOPGPException.BadData { - PGPPublicKeyRingCollection certificates; - try { - certificates = PGPainless.readKeyRing().publicKeyRingCollection(cert); - } catch (IOException | PGPException e) { - throw new SOPGPException.BadData(e); - } - options.addVerificationCerts(certificates); - return this; - } - - @Override - public VerifyImpl signatures(InputStream signatures) throws SOPGPException.BadData { - try { - options.addVerificationOfDetachedSignatures(signatures); - } catch (IOException | PGPException e) { - throw new SOPGPException.BadData(e); - } - return this; - } - - @Override - public List data(InputStream data) throws IOException, SOPGPException.NoSignature, SOPGPException.BadData { - DecryptionStream decryptionStream; - try { - decryptionStream = PGPainless.decryptAndOrVerify() - .onInputStream(data) - .withOptions(options); - - Streams.drain(decryptionStream); - decryptionStream.close(); - - OpenPgpMetadata metadata = decryptionStream.getResult(); - List verificationList = new ArrayList<>(); - - for (SubkeyIdentifier verifiedSigningKey : metadata.getVerifiedSignatures().keySet()) { - PGPSignature signature = metadata.getVerifiedSignatures().get(verifiedSigningKey); - verificationList.add(new Verification( - signature.getCreationTime(), - verifiedSigningKey.getSubkeyFingerprint().toString(), - verifiedSigningKey.getPrimaryKeyFingerprint().toString())); - } - - if (!options.getCertificates().isEmpty()) { - if (verificationList.isEmpty()) { - throw new SOPGPException.NoSignature(); - } - } - - return verificationList; - } catch (PGPException e) { - throw new SOPGPException.BadData(e); - } - } -} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java deleted file mode 100644 index b08b2466..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java +++ /dev/null @@ -1,36 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.sop; - -import java.io.IOException; -import java.io.InputStream; -import java.util.Properties; - -import sop.operation.Version; - -public class VersionImpl implements Version { - @Override - public String getName() { - return "PGPainless-SOP"; - } - - @Override - public String getVersion() { - // See https://stackoverflow.com/a/50119235 - String version; - try { - Properties properties = new Properties(); - InputStream propertiesFileIn = getClass().getResourceAsStream("/version.properties"); - if (propertiesFileIn == null) { - throw new IOException("File version.properties not found."); - } - properties.load(propertiesFileIn); - version = properties.getProperty("version"); - } catch (IOException e) { - version = "DEVELOPMENT"; - } - return version; - } -} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/package-info.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/package-info.java deleted file mode 100644 index c0ce9cda..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Implementation of the java-sop package using pgpainless-core. - */ -package org.pgpainless.sop; diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ArmorImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ArmorImpl.kt new file mode 100644 index 00000000..40ac811d --- /dev/null +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ArmorImpl.kt @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop + +import java.io.BufferedOutputStream +import java.io.InputStream +import java.io.OutputStream +import kotlin.jvm.Throws +import org.bouncycastle.util.io.Streams +import org.pgpainless.decryption_verification.OpenPgpInputStream +import org.pgpainless.util.ArmoredOutputStreamFactory +import sop.Ready +import sop.exception.SOPGPException +import sop.operation.Armor + +/** Implementation of the `armor` operation using PGPainless. */ +class ArmorImpl : Armor { + + @Throws(SOPGPException.BadData::class) + override fun data(data: InputStream): Ready { + return object : Ready() { + override fun writeTo(outputStream: OutputStream) { + // By buffering the output stream, we can improve performance drastically + val bufferedOutputStream = BufferedOutputStream(outputStream) + + // Determine the nature of the given data + val openPgpIn = OpenPgpInputStream(data) + openPgpIn.reset() + + if (openPgpIn.isAsciiArmored) { + // armoring already-armored data is an idempotent operation + Streams.pipeAll(openPgpIn, bufferedOutputStream) + bufferedOutputStream.flush() + openPgpIn.close() + return + } + + val armor = ArmoredOutputStreamFactory.get(bufferedOutputStream) + Streams.pipeAll(openPgpIn, armor) + bufferedOutputStream.flush() + armor.close() + openPgpIn.close() + } + } + } +} diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ChangeKeyPasswordImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ChangeKeyPasswordImpl.kt new file mode 100644 index 00000000..a9aaf1e4 --- /dev/null +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ChangeKeyPasswordImpl.kt @@ -0,0 +1,81 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop + +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPSecretKeyRingCollection +import org.pgpainless.bouncycastle.extensions.openPgpFingerprint +import org.pgpainless.exception.MissingPassphraseException +import org.pgpainless.key.protection.SecretKeyRingProtector +import org.pgpainless.key.util.KeyRingUtils +import org.pgpainless.util.ArmoredOutputStreamFactory +import org.pgpainless.util.Passphrase +import sop.Ready +import sop.exception.SOPGPException +import sop.operation.ChangeKeyPassword + +/** Implementation of the `change-key-password` operation using PGPainless. */ +class ChangeKeyPasswordImpl : ChangeKeyPassword { + + private val oldProtector = MatchMakingSecretKeyRingProtector() + private var newPassphrase = Passphrase.emptyPassphrase() + private var armor = true + + override fun keys(keys: InputStream): Ready { + val newProtector = SecretKeyRingProtector.unlockAnyKeyWith(newPassphrase) + val secretKeysCollection = + try { + KeyReader.readSecretKeys(keys, true) + } catch (e: IOException) { + throw SOPGPException.BadData(e) + } + + val updatedSecretKeys = + secretKeysCollection + .map { secretKeys -> + oldProtector.addSecretKey(secretKeys) + try { + return@map KeyRingUtils.changePassphrase( + null, secretKeys, oldProtector, newProtector) + } catch (e: MissingPassphraseException) { + throw SOPGPException.KeyIsProtected( + "Cannot unlock key ${secretKeys.openPgpFingerprint}", e) + } catch (e: PGPException) { + if (e.message?.contains("Exception decrypting key") == true) { + throw SOPGPException.KeyIsProtected( + "Cannot unlock key ${secretKeys.openPgpFingerprint}", e) + } + throw RuntimeException( + "Cannot change passphrase of key ${secretKeys.openPgpFingerprint}", e) + } + } + .let { PGPSecretKeyRingCollection(it) } + + return object : Ready() { + override fun writeTo(outputStream: OutputStream) { + if (armor) { + ArmoredOutputStreamFactory.get(outputStream).use { + updatedSecretKeys.encode(it) + } + } else { + updatedSecretKeys.encode(outputStream) + } + } + } + } + + override fun newKeyPassphrase(newPassphrase: String): ChangeKeyPassword = apply { + this.newPassphrase = Passphrase.fromPassword(newPassphrase) + } + + override fun noArmor(): ChangeKeyPassword = apply { armor = false } + + override fun oldKeyPassphrase(oldPassphrase: String): ChangeKeyPassword = apply { + oldProtector.addPassphrase(Passphrase.fromPassword(oldPassphrase)) + } +} diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DearmorImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DearmorImpl.kt new file mode 100644 index 00000000..9d196004 --- /dev/null +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DearmorImpl.kt @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop + +import java.io.BufferedOutputStream +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream +import org.bouncycastle.openpgp.PGPUtil +import org.bouncycastle.util.io.Streams +import sop.Ready +import sop.exception.SOPGPException +import sop.operation.Dearmor + +/** Implementation of the `dearmor` operation using PGPainless. */ +class DearmorImpl : Dearmor { + + override fun data(data: InputStream): Ready { + val decoder = + try { + PGPUtil.getDecoderStream(data) + } catch (e: IOException) { + throw SOPGPException.BadData(e) + } + + return object : Ready() { + override fun writeTo(outputStream: OutputStream) { + BufferedOutputStream(outputStream).use { + Streams.pipeAll(decoder, it) + it.flush() + decoder.close() + } + } + } + } +} diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DecryptImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DecryptImpl.kt new file mode 100644 index 00000000..de2b2b3c --- /dev/null +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DecryptImpl.kt @@ -0,0 +1,125 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop + +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream +import java.util.* +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.util.io.Streams +import org.pgpainless.PGPainless +import org.pgpainless.algorithm.SymmetricKeyAlgorithm +import org.pgpainless.decryption_verification.ConsumerOptions +import org.pgpainless.exception.MalformedOpenPgpMessageException +import org.pgpainless.exception.MissingDecryptionMethodException +import org.pgpainless.exception.WrongPassphraseException +import org.pgpainless.util.Passphrase +import sop.DecryptionResult +import sop.ReadyWithResult +import sop.SessionKey +import sop.exception.SOPGPException +import sop.operation.Decrypt +import sop.util.UTF8Util + +/** Implementation of the `decrypt` operation using PGPainless. */ +class DecryptImpl : Decrypt { + + private val consumerOptions = ConsumerOptions.get() + private val protector = MatchMakingSecretKeyRingProtector() + + override fun ciphertext(ciphertext: InputStream): ReadyWithResult { + if (consumerOptions.getDecryptionKeys().isEmpty() && + consumerOptions.getDecryptionPassphrases().isEmpty() && + consumerOptions.getSessionKey() == null) { + throw SOPGPException.MissingArg("Missing decryption key, passphrase or session key.") + } + + val decryptionStream = + try { + PGPainless.decryptAndOrVerify() + .onInputStream(ciphertext) + .withOptions(consumerOptions) + } catch (e: MissingDecryptionMethodException) { + throw SOPGPException.CannotDecrypt( + "No usable decryption key or password provided.", e) + } catch (e: WrongPassphraseException) { + throw SOPGPException.KeyIsProtected() + } catch (e: MalformedOpenPgpMessageException) { + throw SOPGPException.BadData(e) + } catch (e: PGPException) { + throw SOPGPException.BadData(e) + } catch (e: IOException) { + throw SOPGPException.BadData(e) + } finally { + // Forget passphrases after decryption + protector.clear() + } + + return object : ReadyWithResult() { + override fun writeTo(outputStream: OutputStream): DecryptionResult { + Streams.pipeAll(decryptionStream, outputStream) + decryptionStream.close() + + val metadata = decryptionStream.metadata + if (!metadata.isEncrypted) { + throw SOPGPException.BadData("Data is not encrypted.") + } + + val verificationList = + metadata.verifiedInlineSignatures.map { VerificationHelper.mapVerification(it) } + + var sessionKey: SessionKey? = null + if (metadata.sessionKey != null) { + sessionKey = + SessionKey( + metadata.sessionKey!!.algorithm.algorithmId.toByte(), + metadata.sessionKey!!.key) + } + return DecryptionResult(sessionKey, verificationList) + } + } + } + + override fun verifyNotAfter(timestamp: Date): Decrypt = apply { + consumerOptions.verifyNotAfter(timestamp) + } + + override fun verifyNotBefore(timestamp: Date): Decrypt = apply { + consumerOptions.verifyNotBefore(timestamp) + } + + override fun verifyWithCert(cert: InputStream): Decrypt = apply { + KeyReader.readPublicKeys(cert, true).let { consumerOptions.addVerificationCerts(it) } + } + + override fun withKey(key: InputStream): Decrypt = apply { + KeyReader.readSecretKeys(key, true).forEach { + protector.addSecretKey(it) + consumerOptions.addDecryptionKey(it, protector) + } + } + + override fun withKeyPassword(password: ByteArray): Decrypt = apply { + protector.addPassphrase(Passphrase.fromPassword(String(password, UTF8Util.UTF8))) + } + + override fun withPassword(password: String): Decrypt = apply { + consumerOptions.addMessagePassphrase(Passphrase.fromPassword(password)) + password.trimEnd().let { + if (it != password) { + consumerOptions.addMessagePassphrase(Passphrase.fromPassword(it)) + } + } + } + + override fun withSessionKey(sessionKey: SessionKey): Decrypt = apply { + consumerOptions.setSessionKey(mapSessionKey(sessionKey)) + } + + private fun mapSessionKey(sessionKey: SessionKey): org.pgpainless.util.SessionKey = + org.pgpainless.util.SessionKey( + SymmetricKeyAlgorithm.requireFromId(sessionKey.algorithm.toInt()), sessionKey.key) +} diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DetachedSignImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DetachedSignImpl.kt new file mode 100644 index 00000000..19bc782b --- /dev/null +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DetachedSignImpl.kt @@ -0,0 +1,129 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop + +import java.io.InputStream +import java.io.OutputStream +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPSecretKeyRing +import org.bouncycastle.openpgp.PGPSignature +import org.bouncycastle.util.io.Streams +import org.pgpainless.PGPainless +import org.pgpainless.algorithm.CompressionAlgorithm +import org.pgpainless.algorithm.DocumentSignatureType +import org.pgpainless.algorithm.HashAlgorithm +import org.pgpainless.bouncycastle.extensions.openPgpFingerprint +import org.pgpainless.encryption_signing.ProducerOptions +import org.pgpainless.encryption_signing.SigningOptions +import org.pgpainless.exception.KeyException.MissingSecretKeyException +import org.pgpainless.exception.KeyException.UnacceptableSigningKeyException +import org.pgpainless.util.ArmoredOutputStreamFactory +import org.pgpainless.util.Passphrase +import sop.MicAlg +import sop.ReadyWithResult +import sop.SigningResult +import sop.enums.SignAs +import sop.exception.SOPGPException +import sop.operation.DetachedSign +import sop.util.UTF8Util + +/** Implementation of the `sign` operation using PGPainless. */ +class DetachedSignImpl : DetachedSign { + + private val signingOptions = SigningOptions.get() + private val protector = MatchMakingSecretKeyRingProtector() + private val signingKeys = mutableListOf() + + private var armor = true + private var mode = SignAs.binary + + override fun data(data: InputStream): ReadyWithResult { + signingKeys.forEach { + try { + signingOptions.addDetachedSignature(protector, it, modeToSigType(mode)) + } catch (e: UnacceptableSigningKeyException) { + throw SOPGPException.KeyCannotSign("Key ${it.openPgpFingerprint} cannot sign.", e) + } catch (e: MissingSecretKeyException) { + throw SOPGPException.KeyCannotSign( + "Key ${it.openPgpFingerprint} cannot sign. Missing secret key.", e) + } catch (e: PGPException) { + throw SOPGPException.KeyIsProtected( + "Key ${it.openPgpFingerprint} cannot be unlocked.", e) + } + } + + try { + val signingStream = + PGPainless.encryptAndOrSign() + .discardOutput() + .withOptions( + ProducerOptions.sign(signingOptions) + .setAsciiArmor(armor) + .overrideCompressionAlgorithm(CompressionAlgorithm.UNCOMPRESSED)) + + return object : ReadyWithResult() { + override fun writeTo(outputStream: OutputStream): SigningResult { + check(!signingStream.isClosed) { "The operation is a one-shot object." } + + Streams.pipeAll(data, signingStream) + signingStream.close() + val result = signingStream.result + + // forget passphrases + protector.clear() + + val signatures = result.detachedSignatures.map { it.value }.flatten() + val out = + if (armor) ArmoredOutputStreamFactory.get(outputStream) else outputStream + + signatures.forEach { it.encode(out) } + out.close() + outputStream.close() + + return SigningResult.builder() + .setMicAlg(micAlgFromSignatures(signatures)) + .build() + } + } + } catch (e: PGPException) { + throw RuntimeException(e) + } + } + + override fun key(key: InputStream): DetachedSign = apply { + KeyReader.readSecretKeys(key, true).forEach { + val info = PGPainless.inspectKeyRing(it) + if (!info.isUsableForSigning) { + throw SOPGPException.KeyCannotSign( + "Key ${info.fingerprint} does not have valid, signing capable subkeys.") + } + protector.addSecretKey(it) + signingKeys.add(it) + } + } + + override fun mode(mode: SignAs): DetachedSign = apply { this.mode = mode } + + override fun noArmor(): DetachedSign = apply { armor = false } + + override fun withKeyPassword(password: ByteArray): DetachedSign = apply { + protector.addPassphrase(Passphrase.fromPassword(String(password, UTF8Util.UTF8))) + } + + private fun modeToSigType(mode: SignAs): DocumentSignatureType { + return when (mode) { + SignAs.binary -> DocumentSignatureType.BINARY_DOCUMENT + SignAs.text -> DocumentSignatureType.CANONICAL_TEXT_DOCUMENT + } + } + + private fun micAlgFromSignatures(signatures: List): MicAlg = + signatures + .mapNotNull { HashAlgorithm.fromId(it.hashAlgorithm) } + .toSet() + .singleOrNull() + ?.let { MicAlg.fromHashAlgorithmId(it.algorithmId) } + ?: MicAlg.empty() +} diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DetachedVerifyImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DetachedVerifyImpl.kt new file mode 100644 index 00000000..08472144 --- /dev/null +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DetachedVerifyImpl.kt @@ -0,0 +1,71 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop + +import java.io.IOException +import java.io.InputStream +import java.util.* +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.util.io.Streams +import org.pgpainless.PGPainless +import org.pgpainless.decryption_verification.ConsumerOptions +import org.pgpainless.exception.MalformedOpenPgpMessageException +import sop.Verification +import sop.exception.SOPGPException +import sop.operation.DetachedVerify +import sop.operation.VerifySignatures + +/** Implementation of the `verify` operation using PGPainless. */ +class DetachedVerifyImpl : DetachedVerify { + + private val options = ConsumerOptions.get().forceNonOpenPgpData() + + override fun cert(cert: InputStream): DetachedVerify = apply { + options.addVerificationCerts(KeyReader.readPublicKeys(cert, true)) + } + + override fun data(data: InputStream): List { + try { + val verificationStream = + PGPainless.decryptAndOrVerify().onInputStream(data).withOptions(options) + + Streams.drain(verificationStream) + verificationStream.close() + + val result = verificationStream.metadata + val verifications = + result.verifiedDetachedSignatures.map { VerificationHelper.mapVerification(it) } + + if (options.getCertificateSource().getExplicitCertificates().isNotEmpty() && + verifications.isEmpty()) { + throw SOPGPException.NoSignature() + } + + return verifications + } catch (e: MalformedOpenPgpMessageException) { + throw SOPGPException.BadData(e) + } catch (e: PGPException) { + throw SOPGPException.BadData(e) + } + } + + override fun notAfter(timestamp: Date): DetachedVerify = apply { + options.verifyNotAfter(timestamp) + } + + override fun notBefore(timestamp: Date): DetachedVerify = apply { + options.verifyNotBefore(timestamp) + } + + override fun signatures(signatures: InputStream): VerifySignatures = apply { + try { + options.addVerificationOfDetachedSignatures(signatures) + } catch (e: IOException) { + throw SOPGPException.BadData(e) + } catch (e: PGPException) { + throw SOPGPException.BadData(e) + } + } +} diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/EncryptImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/EncryptImpl.kt new file mode 100644 index 00000000..b227561e --- /dev/null +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/EncryptImpl.kt @@ -0,0 +1,155 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop + +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPSecretKeyRing +import org.bouncycastle.util.io.Streams +import org.pgpainless.PGPainless +import org.pgpainless.algorithm.DocumentSignatureType +import org.pgpainless.algorithm.StreamEncoding +import org.pgpainless.bouncycastle.extensions.openPgpFingerprint +import org.pgpainless.encryption_signing.EncryptionOptions +import org.pgpainless.encryption_signing.ProducerOptions +import org.pgpainless.encryption_signing.SigningOptions +import org.pgpainless.exception.KeyException.UnacceptableEncryptionKeyException +import org.pgpainless.exception.KeyException.UnacceptableSigningKeyException +import org.pgpainless.exception.WrongPassphraseException +import org.pgpainless.util.Passphrase +import sop.EncryptionResult +import sop.Profile +import sop.ReadyWithResult +import sop.enums.EncryptAs +import sop.exception.SOPGPException +import sop.operation.Encrypt +import sop.util.UTF8Util + +/** Implementation of the `encrypt` operation using PGPainless. */ +class EncryptImpl : Encrypt { + + companion object { + @JvmField val RFC4880_PROFILE = Profile("rfc4880", "Follow the packet format of rfc4880") + + @JvmField val SUPPORTED_PROFILES = listOf(RFC4880_PROFILE) + } + + private val encryptionOptions = EncryptionOptions.get() + private var signingOptions: SigningOptions? = null + private val signingKeys = mutableListOf() + private val protector = MatchMakingSecretKeyRingProtector() + + private var profile = RFC4880_PROFILE.name + private var mode = EncryptAs.binary + private var armor = true + + override fun mode(mode: EncryptAs): Encrypt = apply { this.mode = mode } + + override fun noArmor(): Encrypt = apply { this.armor = false } + + override fun plaintext(plaintext: InputStream): ReadyWithResult { + if (!encryptionOptions.hasEncryptionMethod()) { + throw SOPGPException.MissingArg("Missing encryption method.") + } + + val options = + if (signingOptions != null) { + ProducerOptions.signAndEncrypt(encryptionOptions, signingOptions!!) + } else { + ProducerOptions.encrypt(encryptionOptions) + } + .setAsciiArmor(armor) + .setEncoding(modeToStreamEncoding(mode)) + + signingKeys.forEach { + try { + signingOptions!!.addInlineSignature(protector, it, modeToSignatureType(mode)) + } catch (e: UnacceptableSigningKeyException) { + throw SOPGPException.KeyCannotSign("Key ${it.openPgpFingerprint} cannot sign", e) + } catch (e: WrongPassphraseException) { + throw SOPGPException.KeyIsProtected("Cannot unlock key ${it.openPgpFingerprint}", e) + } catch (e: PGPException) { + throw SOPGPException.BadData(e) + } + } + + try { + return object : ReadyWithResult() { + override fun writeTo(outputStream: OutputStream): EncryptionResult { + val encryptionStream = + PGPainless.encryptAndOrSign() + .onOutputStream(outputStream) + .withOptions(options) + Streams.pipeAll(plaintext, encryptionStream) + encryptionStream.close() + // TODO: Extract and emit session key once BC supports that + return EncryptionResult(null) + } + } + } catch (e: PGPException) { + throw IOException(e) + } + } + + override fun profile(profileName: String): Encrypt = apply { + profile = + SUPPORTED_PROFILES.find { it.name == profileName }?.name + ?: throw SOPGPException.UnsupportedProfile("encrypt", profileName) + } + + override fun signWith(key: InputStream): Encrypt = apply { + if (signingOptions == null) { + signingOptions = SigningOptions.get() + } + + val signingKey = + KeyReader.readSecretKeys(key, true).singleOrNull() + ?: throw SOPGPException.BadData( + AssertionError( + "Exactly one secret key at a time expected. Got zero or multiple instead.")) + + val info = PGPainless.inspectKeyRing(signingKey) + if (info.signingSubkeys.isEmpty()) { + throw SOPGPException.KeyCannotSign("Key ${info.fingerprint} cannot sign.") + } + + protector.addSecretKey(signingKey) + signingKeys.add(signingKey) + } + + override fun withCert(cert: InputStream): Encrypt = apply { + try { + encryptionOptions.addRecipients(KeyReader.readPublicKeys(cert, true)) + } catch (e: UnacceptableEncryptionKeyException) { + throw SOPGPException.CertCannotEncrypt(e.message ?: "Cert cannot encrypt", e) + } catch (e: IOException) { + throw SOPGPException.BadData(e) + } + } + + override fun withKeyPassword(password: ByteArray): Encrypt = apply { + protector.addPassphrase(Passphrase.fromPassword(String(password, UTF8Util.UTF8))) + } + + override fun withPassword(password: String): Encrypt = apply { + encryptionOptions.addMessagePassphrase(Passphrase.fromPassword(password)) + } + + private fun modeToStreamEncoding(mode: EncryptAs): StreamEncoding { + return when (mode) { + EncryptAs.binary -> StreamEncoding.BINARY + EncryptAs.text -> StreamEncoding.UTF8 + } + } + + private fun modeToSignatureType(mode: EncryptAs): DocumentSignatureType { + return when (mode) { + EncryptAs.binary -> DocumentSignatureType.BINARY_DOCUMENT + EncryptAs.text -> DocumentSignatureType.CANONICAL_TEXT_DOCUMENT + } + } +} diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ExtractCertImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ExtractCertImpl.kt new file mode 100644 index 00000000..7fe66ee5 --- /dev/null +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ExtractCertImpl.kt @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop + +import java.io.InputStream +import java.io.OutputStream +import org.pgpainless.PGPainless +import org.pgpainless.util.ArmorUtils +import org.pgpainless.util.ArmoredOutputStreamFactory +import sop.Ready +import sop.operation.ExtractCert + +/** Implementation of the `extract-cert` operation using PGPainless. */ +class ExtractCertImpl : ExtractCert { + + private var armor = true + + override fun key(keyInputStream: InputStream): Ready { + val certs = + KeyReader.readSecretKeys(keyInputStream, true).map { PGPainless.extractCertificate(it) } + + return object : Ready() { + override fun writeTo(outputStream: OutputStream) { + if (armor) { + if (certs.size == 1) { + val cert = certs[0] + // This way we have a nice armor header with fingerprint and user-ids + val armorOut = ArmorUtils.toAsciiArmoredStream(cert, outputStream) + cert.encode(armorOut) + armorOut.close() + } else { + // for multiple certs, add no info headers to the ASCII armor + val armorOut = ArmoredOutputStreamFactory.get(outputStream) + certs.forEach { it.encode(armorOut) } + armorOut.close() + } + } else { + certs.forEach { it.encode(outputStream) } + } + } + } + } + + override fun noArmor(): ExtractCert = apply { armor = false } +} diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/GenerateKeyImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/GenerateKeyImpl.kt new file mode 100644 index 00000000..f8297c56 --- /dev/null +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/GenerateKeyImpl.kt @@ -0,0 +1,137 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop + +import java.io.OutputStream +import java.lang.RuntimeException +import java.security.InvalidAlgorithmParameterException +import java.security.NoSuchAlgorithmException +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPSecretKeyRing +import org.pgpainless.PGPainless +import org.pgpainless.algorithm.KeyFlag +import org.pgpainless.key.generation.KeyRingBuilder +import org.pgpainless.key.generation.KeySpec +import org.pgpainless.key.generation.type.KeyType +import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve +import org.pgpainless.key.generation.type.rsa.RsaLength +import org.pgpainless.key.generation.type.xdh_legacy.XDHLegacySpec +import org.pgpainless.util.ArmorUtils +import org.pgpainless.util.Passphrase +import sop.Profile +import sop.Ready +import sop.exception.SOPGPException +import sop.operation.GenerateKey + +/** Implementation of the `generate-key` operation using PGPainless. */ +class GenerateKeyImpl : GenerateKey { + + companion object { + @JvmField + val CURVE25519_PROFILE = + Profile( + "draft-koch-eddsa-for-openpgp-00", "Generate EdDSA / ECDH keys using Curve25519") + @JvmField val RSA4096_PROFILE = Profile("rfc4880", "Generate 4096-bit RSA keys") + + @JvmField val SUPPORTED_PROFILES = listOf(CURVE25519_PROFILE, RSA4096_PROFILE) + } + + private val userIds = mutableSetOf() + private var armor = true + private var signingOnly = false + private var passphrase = Passphrase.emptyPassphrase() + private var profile = CURVE25519_PROFILE.name + + override fun generate(): Ready { + try { + val key = generateKeyWithProfile(profile, userIds, passphrase, signingOnly) + return object : Ready() { + override fun writeTo(outputStream: OutputStream) { + if (armor) { + val armorOut = ArmorUtils.toAsciiArmoredStream(key, outputStream) + key.encode(armorOut) + armorOut.close() + } else { + key.encode(outputStream) + } + } + } + } catch (e: InvalidAlgorithmParameterException) { + throw SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", e) + } catch (e: NoSuchAlgorithmException) { + throw SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", e) + } catch (e: PGPException) { + throw RuntimeException(e) + } + } + + override fun noArmor(): GenerateKey = apply { armor = false } + + override fun profile(profile: String): GenerateKey = apply { + this.profile = + SUPPORTED_PROFILES.find { it.name == profile }?.name + ?: throw SOPGPException.UnsupportedProfile("generate-key", profile) + } + + override fun signingOnly(): GenerateKey = apply { signingOnly = true } + + override fun userId(userId: String): GenerateKey = apply { userIds.add(userId) } + + override fun withKeyPassword(password: String): GenerateKey = apply { + this.passphrase = Passphrase.fromPassword(password) + } + + private fun generateKeyWithProfile( + profile: String, + userIds: Set, + passphrase: Passphrase, + signingOnly: Boolean + ): PGPSecretKeyRing { + val keyBuilder: KeyRingBuilder = + when (profile) { + CURVE25519_PROFILE.name -> + PGPainless.buildKeyRing() + .setPrimaryKey( + KeySpec.getBuilder( + KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), + KeyFlag.CERTIFY_OTHER)) + .addSubkey( + KeySpec.getBuilder( + KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.SIGN_DATA)) + .apply { + if (!signingOnly) { + addSubkey( + KeySpec.getBuilder( + KeyType.XDH_LEGACY(XDHLegacySpec._X25519), + KeyFlag.ENCRYPT_COMMS, + KeyFlag.ENCRYPT_STORAGE)) + } + } + RSA4096_PROFILE.name -> { + PGPainless.buildKeyRing() + .setPrimaryKey( + KeySpec.getBuilder(KeyType.RSA(RsaLength._4096), KeyFlag.CERTIFY_OTHER)) + .addSubkey( + KeySpec.getBuilder(KeyType.RSA(RsaLength._4096), KeyFlag.SIGN_DATA)) + .apply { + if (!signingOnly) { + addSubkey( + KeySpec.getBuilder( + KeyType.RSA(RsaLength._4096), + KeyFlag.ENCRYPT_COMMS, + KeyFlag.ENCRYPT_STORAGE)) + } + } + } + else -> throw SOPGPException.UnsupportedProfile("generate-key", profile) + } + + userIds.forEach { keyBuilder.addUserId(it) } + if (!passphrase.isEmpty) { + keyBuilder.setPassphrase(passphrase) + } + return keyBuilder.build() + } +} diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineDetachImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineDetachImpl.kt new file mode 100644 index 00000000..88ca8c54 --- /dev/null +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineDetachImpl.kt @@ -0,0 +1,134 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop + +import java.io.ByteArrayOutputStream +import java.io.InputStream +import java.io.OutputStream +import org.bouncycastle.bcpg.ArmoredInputStream +import org.bouncycastle.openpgp.PGPCompressedData +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPLiteralData +import org.bouncycastle.openpgp.PGPOnePassSignatureList +import org.bouncycastle.openpgp.PGPSignatureList +import org.bouncycastle.util.io.Streams +import org.pgpainless.decryption_verification.OpenPgpInputStream +import org.pgpainless.decryption_verification.cleartext_signatures.ClearsignedMessageUtil +import org.pgpainless.exception.WrongConsumingMethodException +import org.pgpainless.implementation.ImplementationFactory +import org.pgpainless.util.ArmoredOutputStreamFactory +import sop.ReadyWithResult +import sop.Signatures +import sop.exception.SOPGPException +import sop.operation.InlineDetach + +/** Implementation of the `inline-detach` operation using PGPainless. */ +class InlineDetachImpl : InlineDetach { + + private var armor = true + + override fun message(messageInputStream: InputStream): ReadyWithResult { + return object : ReadyWithResult() { + + private val sigOut = ByteArrayOutputStream() + + override fun writeTo(outputStream: OutputStream): Signatures { + var pgpIn = OpenPgpInputStream(messageInputStream) + if (pgpIn.isNonOpenPgp) { + throw SOPGPException.BadData("Data appears to be non-OpenPGP.") + } + var signatures: PGPSignatureList? = null + + // Handle ASCII armor + if (pgpIn.isAsciiArmored) { + val armorIn = ArmoredInputStream(pgpIn) + + // Handle cleartext signature framework + if (armorIn.isClearText) { + try { + signatures = + ClearsignedMessageUtil.detachSignaturesFromInbandClearsignedMessage( + armorIn, outputStream) + if (signatures.isEmpty) { + throw SOPGPException.BadData( + "Data did not contain OpenPGP signatures.") + } + } catch (e: WrongConsumingMethodException) { + throw SOPGPException.BadData(e) + } + } + + // else just dearmor + pgpIn = OpenPgpInputStream(armorIn) + } + + // If data was not using cleartext signature framework + if (signatures == null) { + if (!pgpIn.isBinaryOpenPgp) { + throw SOPGPException.BadData( + "Data was containing ASCII armored non-OpenPGP data.") + } + + // handle binary OpenPGP data + var objectFactory = + ImplementationFactory.getInstance().getPGPObjectFactory(pgpIn) + var next: Any? + + while (objectFactory.nextObject().also { next = it } != null) { + + if (next is PGPOnePassSignatureList) { + // Skip over OPSs + continue + } + + if (next is PGPLiteralData) { + // Write out contents of Literal Data packet + val literalIn = (next as PGPLiteralData).dataStream + Streams.pipeAll(literalIn, outputStream) + literalIn.close() + continue + } + + if (next is PGPCompressedData) { + // Decompress compressed data + try { + objectFactory = + ImplementationFactory.getInstance() + .getPGPObjectFactory((next as PGPCompressedData).dataStream) + } catch (e: PGPException) { + throw SOPGPException.BadData( + "Cannot decompress PGPCompressedData", e) + } + } + + if (next is PGPSignatureList) { + signatures = next as PGPSignatureList + } + } + } + + if (signatures == null) { + throw SOPGPException.BadData("Data did not contain OpenPGP signatures.") + } + + if (armor) { + ArmoredOutputStreamFactory.get(sigOut).use { armoredOut -> + signatures.forEach { it.encode(armoredOut) } + } + } else { + signatures.forEach { it.encode(sigOut) } + } + + return object : Signatures() { + override fun writeTo(outputStream: OutputStream) { + sigOut.writeTo(outputStream) + } + } + } + } + } + + override fun noArmor(): InlineDetach = apply { armor = false } +} diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineSignImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineSignImpl.kt new file mode 100644 index 00000000..bd77b553 --- /dev/null +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineSignImpl.kt @@ -0,0 +1,126 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop + +import java.io.InputStream +import java.io.OutputStream +import java.lang.RuntimeException +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPSecretKeyRing +import org.bouncycastle.util.io.Streams +import org.pgpainless.PGPainless +import org.pgpainless.algorithm.CompressionAlgorithm +import org.pgpainless.algorithm.DocumentSignatureType +import org.pgpainless.algorithm.StreamEncoding +import org.pgpainless.bouncycastle.extensions.openPgpFingerprint +import org.pgpainless.encryption_signing.ProducerOptions +import org.pgpainless.encryption_signing.SigningOptions +import org.pgpainless.exception.KeyException.MissingSecretKeyException +import org.pgpainless.exception.KeyException.UnacceptableSigningKeyException +import org.pgpainless.util.Passphrase +import sop.Ready +import sop.enums.InlineSignAs +import sop.exception.SOPGPException +import sop.operation.InlineSign +import sop.util.UTF8Util + +/** Implementation of the `inline-sign` operation using PGPainless. */ +class InlineSignImpl : InlineSign { + + private val signingOptions = SigningOptions.get() + private val protector = MatchMakingSecretKeyRingProtector() + private val signingKeys = mutableListOf() + + private var armor = true + private var mode = InlineSignAs.binary + + override fun data(data: InputStream): Ready { + signingKeys.forEach { key -> + try { + if (mode == InlineSignAs.clearsigned) { + signingOptions.addDetachedSignature(protector, key, modeToSigType(mode)) + } else { + signingOptions.addInlineSignature(protector, key, modeToSigType(mode)) + } + } catch (e: UnacceptableSigningKeyException) { + throw SOPGPException.KeyCannotSign("Key ${key.openPgpFingerprint} cannot sign.", e) + } catch (e: MissingSecretKeyException) { + throw SOPGPException.KeyCannotSign( + "Key ${key.openPgpFingerprint} does not have the secret signing key component available.", + e) + } catch (e: PGPException) { + throw SOPGPException.KeyIsProtected( + "Key ${key.openPgpFingerprint} cannot be unlocked.", e) + } + } + + val producerOptions = + ProducerOptions.sign(signingOptions).apply { + when (mode) { + InlineSignAs.clearsigned -> { + setCleartextSigned() + setAsciiArmor(true) // CSF is always armored + setEncoding(StreamEncoding.TEXT) + applyCRLFEncoding() + } + InlineSignAs.text -> { + setEncoding(StreamEncoding.TEXT) + applyCRLFEncoding() + } + else -> { + setAsciiArmor(armor) + } + } + overrideCompressionAlgorithm(CompressionAlgorithm.UNCOMPRESSED) + } + + return object : Ready() { + override fun writeTo(outputStream: OutputStream) { + try { + val signingStream = + PGPainless.encryptAndOrSign() + .onOutputStream(outputStream) + .withOptions(producerOptions) + + Streams.pipeAll(data, signingStream) + signingStream.close() + + // forget passphrases + protector.clear() + } catch (e: PGPException) { + throw RuntimeException(e) + } + } + } + } + + override fun key(key: InputStream): InlineSign = apply { + KeyReader.readSecretKeys(key, true).forEach { + val info = PGPainless.inspectKeyRing(it) + if (!info.isUsableForSigning) { + throw SOPGPException.KeyCannotSign( + "Key ${info.fingerprint} does not have valid, signing capable subkeys.") + } + protector.addSecretKey(it) + signingKeys.add(it) + } + } + + override fun mode(mode: InlineSignAs): InlineSign = apply { this.mode = mode } + + override fun noArmor(): InlineSign = apply { armor = false } + + override fun withKeyPassword(password: ByteArray): InlineSign = apply { + protector.addPassphrase(Passphrase.fromPassword(String(password, UTF8Util.UTF8))) + } + + private fun modeToSigType(mode: InlineSignAs): DocumentSignatureType { + return when (mode) { + InlineSignAs.binary -> DocumentSignatureType.BINARY_DOCUMENT + InlineSignAs.text -> DocumentSignatureType.CANONICAL_TEXT_DOCUMENT + InlineSignAs.clearsigned -> DocumentSignatureType.CANONICAL_TEXT_DOCUMENT + } + } +} diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineVerifyImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineVerifyImpl.kt new file mode 100644 index 00000000..0b4e7d2f --- /dev/null +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineVerifyImpl.kt @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop + +import java.io.InputStream +import java.io.OutputStream +import java.util.* +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.util.io.Streams +import org.pgpainless.PGPainless +import org.pgpainless.decryption_verification.ConsumerOptions +import org.pgpainless.exception.MalformedOpenPgpMessageException +import org.pgpainless.exception.MissingDecryptionMethodException +import sop.ReadyWithResult +import sop.Verification +import sop.exception.SOPGPException +import sop.operation.InlineVerify + +/** Implementation of the `inline-verify` operation using PGPainless. */ +class InlineVerifyImpl : InlineVerify { + + private val options = ConsumerOptions.get() + + override fun cert(cert: InputStream): InlineVerify = apply { + options.addVerificationCerts(KeyReader.readPublicKeys(cert, true)) + } + + override fun data(data: InputStream): ReadyWithResult> { + return object : ReadyWithResult>() { + override fun writeTo(outputStream: OutputStream): List { + try { + val verificationStream = + PGPainless.decryptAndOrVerify().onInputStream(data).withOptions(options) + + Streams.pipeAll(verificationStream, outputStream) + verificationStream.close() + + val result = verificationStream.metadata + val verifications = + if (result.isUsingCleartextSignatureFramework) { + result.verifiedDetachedSignatures + } else { + result.verifiedInlineSignatures + } + .map { VerificationHelper.mapVerification(it) } + + if (options.getCertificateSource().getExplicitCertificates().isNotEmpty() && + verifications.isEmpty()) { + throw SOPGPException.NoSignature() + } + + return verifications + } catch (e: MissingDecryptionMethodException) { + throw SOPGPException.BadData("Cannot verify encrypted message.", e) + } catch (e: MalformedOpenPgpMessageException) { + throw SOPGPException.BadData(e) + } catch (e: PGPException) { + throw SOPGPException.BadData(e) + } + } + } + } + + override fun notAfter(timestamp: Date): InlineVerify = apply { + options.verifyNotAfter(timestamp) + } + + override fun notBefore(timestamp: Date): InlineVerify = apply { + options.verifyNotBefore(timestamp) + } +} diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/KeyReader.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/KeyReader.kt new file mode 100644 index 00000000..2ce608ca --- /dev/null +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/KeyReader.kt @@ -0,0 +1,77 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop + +import java.io.IOException +import java.io.InputStream +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPPublicKeyRingCollection +import org.bouncycastle.openpgp.PGPRuntimeOperationException +import org.bouncycastle.openpgp.PGPSecretKeyRingCollection +import org.pgpainless.PGPainless +import sop.exception.SOPGPException + +/** Reader for OpenPGP keys and certificates with error matching according to the SOP spec. */ +class KeyReader { + + companion object { + @JvmStatic + fun readSecretKeys( + keyInputStream: InputStream, + requireContent: Boolean + ): PGPSecretKeyRingCollection { + val keys = + try { + PGPainless.readKeyRing().secretKeyRingCollection(keyInputStream) + } catch (e: IOException) { + if (e.message == null) { + throw e + } + if (e.message!!.startsWith("unknown object in stream:") || + e.message!!.startsWith("invalid header encountered")) { + throw SOPGPException.BadData(e) + } + throw e + } + if (requireContent && keys.none()) { + throw SOPGPException.BadData(PGPException("No key data found.")) + } + + return keys + } + + @JvmStatic + fun readPublicKeys( + certIn: InputStream, + requireContent: Boolean + ): PGPPublicKeyRingCollection { + val certs = + try { + PGPainless.readKeyRing().keyRingCollection(certIn, true) + } catch (e: IOException) { + if (e.message == null) { + throw e + } + if (e.message!!.startsWith("unknown object in stream:") || + e.message!!.startsWith("invalid header encountered")) { + throw SOPGPException.BadData(e) + } + throw e + } catch (e: PGPRuntimeOperationException) { + throw SOPGPException.BadData(e) + } + + if (certs.pgpSecretKeyRingCollection.any()) { + throw SOPGPException.BadData( + "Secret key components encountered, while certificates were expected.") + } + + if (requireContent && certs.pgpPublicKeyRingCollection.none()) { + throw SOPGPException.BadData(PGPException("No cert data found.")) + } + return certs.pgpPublicKeyRingCollection + } + } +} diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ListProfilesImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ListProfilesImpl.kt new file mode 100644 index 00000000..39a5151d --- /dev/null +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ListProfilesImpl.kt @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop + +import sop.Profile +import sop.exception.SOPGPException +import sop.operation.ListProfiles + +/** Implementation of the `list-profiles` operation using PGPainless. */ +class ListProfilesImpl : ListProfiles { + + override fun subcommand(command: String): List = + when (command) { + "generate-key" -> GenerateKeyImpl.SUPPORTED_PROFILES + "encrypt" -> EncryptImpl.SUPPORTED_PROFILES + else -> throw SOPGPException.UnsupportedProfile(command) + } +} diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/MatchMakingSecretKeyRingProtector.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/MatchMakingSecretKeyRingProtector.kt new file mode 100644 index 00000000..13347721 --- /dev/null +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/MatchMakingSecretKeyRingProtector.kt @@ -0,0 +1,88 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop + +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPSecretKey +import org.bouncycastle.openpgp.PGPSecretKeyRing +import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor +import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor +import org.pgpainless.bouncycastle.extensions.isDecrypted +import org.pgpainless.bouncycastle.extensions.unlock +import org.pgpainless.key.protection.CachingSecretKeyRingProtector +import org.pgpainless.key.protection.SecretKeyRingProtector +import org.pgpainless.util.Passphrase + +/** + * Implementation of the [SecretKeyRingProtector] which can be handed passphrases and keys + * separately, and which then matches up passphrases and keys when needed. + */ +class MatchMakingSecretKeyRingProtector : SecretKeyRingProtector { + + private val passphrases = mutableSetOf() + private val keys = mutableSetOf() + private val protector = CachingSecretKeyRingProtector() + + fun addPassphrase(passphrase: Passphrase) = apply { + if (passphrase.isEmpty) { + return@apply + } + + if (!passphrases.add(passphrase)) { + return@apply + } + + keys.forEach { key -> + for (subkey in key) { + if (protector.hasPassphrase(subkey.keyID)) { + continue + } + + if (testPassphrase(passphrase, subkey)) { + protector.addPassphrase(subkey.keyID, passphrase) + } + } + } + } + + fun addSecretKey(key: PGPSecretKeyRing) = apply { + if (!keys.add(key)) { + return@apply + } + + key.forEach { subkey -> + if (subkey.isDecrypted()) { + protector.addPassphrase(subkey.keyID, Passphrase.emptyPassphrase()) + } else { + passphrases.forEach { passphrase -> + if (testPassphrase(passphrase, subkey)) { + protector.addPassphrase(subkey.keyID, passphrase) + } + } + } + } + } + + private fun testPassphrase(passphrase: Passphrase, key: PGPSecretKey): Boolean = + try { + key.unlock(passphrase) + true + } catch (e: PGPException) { + // Wrong passphrase + false + } + + override fun hasPassphraseFor(keyId: Long): Boolean = protector.hasPassphrase(keyId) + + override fun getDecryptor(keyId: Long): PBESecretKeyDecryptor? = protector.getDecryptor(keyId) + + override fun getEncryptor(keyId: Long): PBESecretKeyEncryptor? = protector.getEncryptor(keyId) + + /** Clear all known passphrases from the protector. */ + fun clear() { + passphrases.forEach { it.clear() } + keys.forEach { protector.forgetPassphrase(it) } + } +} diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/RevokeKeyImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/RevokeKeyImpl.kt new file mode 100644 index 00000000..ecc87e62 --- /dev/null +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/RevokeKeyImpl.kt @@ -0,0 +1,95 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop + +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream +import java.lang.RuntimeException +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPPublicKeyRing +import org.bouncycastle.openpgp.PGPPublicKeyRingCollection +import org.pgpainless.PGPainless +import org.pgpainless.bouncycastle.extensions.openPgpFingerprint +import org.pgpainless.exception.WrongPassphraseException +import org.pgpainless.key.util.KeyRingUtils +import org.pgpainless.key.util.RevocationAttributes +import org.pgpainless.util.ArmoredOutputStreamFactory +import org.pgpainless.util.Passphrase +import sop.Ready +import sop.exception.SOPGPException +import sop.operation.RevokeKey +import sop.util.UTF8Util + +class RevokeKeyImpl : RevokeKey { + + private val protector = MatchMakingSecretKeyRingProtector() + private var armor = true + + override fun keys(keys: InputStream): Ready { + val secretKeyRings = + try { + KeyReader.readSecretKeys(keys, true) + } catch (e: IOException) { + throw SOPGPException.BadData("Cannot decode secret keys.", e) + } + + secretKeyRings.forEach { protector.addSecretKey(it) } + + val revocationCertificates = mutableListOf() + secretKeyRings.forEach { secretKeys -> + val editor = PGPainless.modifyKeyRing(secretKeys) + try { + val attributes = + RevocationAttributes.createKeyRevocation() + .withReason(RevocationAttributes.Reason.NO_REASON) + .withoutDescription() + if (secretKeys.publicKey.version == 6) { + revocationCertificates.add( + editor.createMinimalRevocationCertificate(protector, attributes)) + } else { + val certificate = PGPainless.extractCertificate(secretKeys) + val revocation = editor.createRevocation(protector, attributes) + revocationCertificates.add( + KeyRingUtils.injectCertification(certificate, revocation)) + } + } catch (e: WrongPassphraseException) { + throw SOPGPException.KeyIsProtected( + "Missing or wrong passphrase for key ${secretKeys.openPgpFingerprint}", e) + } catch (e: PGPException) { + throw RuntimeException( + "Cannot generate revocation certificate for key ${secretKeys.openPgpFingerprint}", + e) + } + } + + return object : Ready() { + override fun writeTo(outputStream: OutputStream) { + val collection = PGPPublicKeyRingCollection(revocationCertificates) + if (armor) { + val armorOut = ArmoredOutputStreamFactory.get(outputStream) + collection.encode(armorOut) + armorOut.close() + } else { + collection.encode(outputStream) + } + } + } + } + + override fun noArmor(): RevokeKey = apply { armor = false } + + override fun withKeyPassword(password: ByteArray): RevokeKey = apply { + val string = + try { + UTF8Util.decodeUTF8(password) + } catch (e: CharacterCodingException) { + // TODO: Add cause + throw SOPGPException.PasswordNotHumanReadable( + "Cannot UTF8-decode password: ${e.stackTraceToString()}") + } + protector.addPassphrase(Passphrase.fromPassword(string)) + } +} diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/SOPImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/SOPImpl.kt new file mode 100644 index 00000000..16f54a22 --- /dev/null +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/SOPImpl.kt @@ -0,0 +1,56 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop + +import sop.SOP +import sop.SOPV +import sop.operation.Armor +import sop.operation.ChangeKeyPassword +import sop.operation.Dearmor +import sop.operation.Decrypt +import sop.operation.DetachedSign +import sop.operation.DetachedVerify +import sop.operation.Encrypt +import sop.operation.ExtractCert +import sop.operation.GenerateKey +import sop.operation.InlineDetach +import sop.operation.InlineSign +import sop.operation.InlineVerify +import sop.operation.ListProfiles +import sop.operation.RevokeKey +import sop.operation.Version + +class SOPImpl(private val sopv: SOPV = SOPVImpl()) : SOP { + + override fun armor(): Armor = ArmorImpl() + + override fun changeKeyPassword(): ChangeKeyPassword = ChangeKeyPasswordImpl() + + override fun dearmor(): Dearmor = DearmorImpl() + + override fun decrypt(): Decrypt = DecryptImpl() + + override fun detachedSign(): DetachedSign = DetachedSignImpl() + + override fun detachedVerify(): DetachedVerify = sopv.detachedVerify() + + override fun encrypt(): Encrypt = EncryptImpl() + + override fun extractCert(): ExtractCert = ExtractCertImpl() + + override fun generateKey(): GenerateKey = GenerateKeyImpl() + + override fun inlineDetach(): InlineDetach = InlineDetachImpl() + + override fun inlineSign(): InlineSign = InlineSignImpl() + + override fun inlineVerify(): InlineVerify = sopv.inlineVerify() + + override fun listProfiles(): ListProfiles = ListProfilesImpl() + + override fun revokeKey(): RevokeKey = RevokeKeyImpl() + + override fun version(): Version = sopv.version() +} diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/SOPVImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/SOPVImpl.kt new file mode 100644 index 00000000..43b4c64f --- /dev/null +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/SOPVImpl.kt @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop + +import org.pgpainless.util.ArmoredOutputStreamFactory +import sop.SOPV +import sop.operation.DetachedVerify +import sop.operation.InlineVerify +import sop.operation.Version + +class SOPVImpl : SOPV { + + init { + ArmoredOutputStreamFactory.setVersionInfo(null) + } + + override fun detachedVerify(): DetachedVerify = DetachedVerifyImpl() + + override fun inlineVerify(): InlineVerify = InlineVerifyImpl() + + override fun version(): Version = VersionImpl() +} diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/VerificationHelper.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/VerificationHelper.kt new file mode 100644 index 00000000..9198e3b7 --- /dev/null +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/VerificationHelper.kt @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop + +import org.bouncycastle.openpgp.PGPSignature +import org.pgpainless.decryption_verification.SignatureVerification +import sop.Verification +import sop.enums.SignatureMode + +/** Helper class for shared methods related to [Verification] objects. */ +class VerificationHelper { + + companion object { + + /** + * Map a [SignatureVerification] object to a [Verification]. + * + * @param sigVerification signature verification + * @return verification + */ + @JvmStatic + fun mapVerification(sigVerification: SignatureVerification): Verification = + Verification( + sigVerification.signature.creationTime, + sigVerification.signingKey.subkeyFingerprint.toString(), + sigVerification.signingKey.primaryKeyFingerprint.toString(), + getMode(sigVerification.signature), + null) + + /** + * Map an OpenPGP signature type to a [SignatureMode] enum. Note: This method only maps + * [PGPSignature.BINARY_DOCUMENT] and [PGPSignature.CANONICAL_TEXT_DOCUMENT]. Other values + * are mapped to `null`. + * + * @param signature signature + * @return signature mode enum or null + */ + @JvmStatic + fun getMode(signature: PGPSignature): SignatureMode? = + when (signature.signatureType) { + PGPSignature.BINARY_DOCUMENT -> SignatureMode.binary + PGPSignature.CANONICAL_TEXT_DOCUMENT -> SignatureMode.text + else -> null + } + } +} diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/VersionImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/VersionImpl.kt new file mode 100644 index 00000000..94b9c016 --- /dev/null +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/VersionImpl.kt @@ -0,0 +1,76 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop + +import java.io.IOException +import java.io.InputStream +import java.util.* +import org.bouncycastle.jce.provider.BouncyCastleProvider +import sop.SOP +import sop.operation.Version + +/** Implementation of the `version` operation using PGPainless. */ +class VersionImpl : Version { + + companion object { + const val SOP_VERSION = 10 + const val SOPV_VERSION = "1.0" + } + + override fun getBackendVersion(): String = "PGPainless ${getVersion()}" + + override fun getExtendedVersion(): String { + val bcVersion = + String.format(Locale.US, "Bouncy Castle %.2f", BouncyCastleProvider().version) + val specVersion = String.format("%02d", SOP_VERSION) + return """${getName()} ${getVersion()} +https://codeberg.org/PGPainless/pgpainless/src/branch/main/pgpainless-sop + +Implementation of the Stateless OpenPGP Protocol Version $specVersion +https://datatracker.ietf.org/doc/html/draft-dkg-openpgp-stateless-cli-$specVersion + +Based on pgpainless-core ${getVersion()} +https://pgpainless.org +${formatSopJavaVersion()} +Using $bcVersion +https://www.bouncycastle.org/java.html""" + } + + override fun getName(): String = "PGPainless-SOP" + + override fun getSopSpecImplementationRemarks(): String? = null + + override fun getSopSpecRevisionNumber(): Int = SOP_VERSION + + override fun getSopVVersion(): String = SOPV_VERSION + + override fun getVersion(): String { + // See https://stackoverflow.com/a/50119235 + return try { + val resourceIn: InputStream = + SOP::class.java.getResourceAsStream("/pgpainless-sop.properties") + ?: throw IOException("File pgpainless-sop.properties not found.") + + val properties = Properties().apply { load(resourceIn) } + properties.getProperty("pgpainless-sop-version") + } catch (e: IOException) { + "DEVELOPMENT" + } + } + + private fun formatSopJavaVersion(): String { + return getSopJavaVersion()?.let { + """ + + sop-java $it + + """ + .trimIndent() + } + ?: "" + } + + override fun isSopSpecImplementationIncomplete(): Boolean = false +} diff --git a/pgpainless-sop/src/main/resources/pgpainless-sop.properties b/pgpainless-sop/src/main/resources/pgpainless-sop.properties new file mode 100644 index 00000000..d0f47796 --- /dev/null +++ b/pgpainless-sop/src/main/resources/pgpainless-sop.properties @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2025 Paul Schaub +# +# SPDX-License-Identifier: Apache-2.0 +pgpainless-sop-version=@project.version@ \ No newline at end of file diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/ArmorTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/ArmorTest.java index ad6da440..82688bbf 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/ArmorTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/ArmorTest.java @@ -4,32 +4,23 @@ package org.pgpainless.sop; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; - -import org.bouncycastle.openpgp.PGPException; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.util.ArmorUtils; -import sop.enums.ArmorLabel; -import sop.exception.SOPGPException; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; public class ArmorTest { @Test - public void labelIsNotSupported() { - assertThrows(SOPGPException.UnsupportedOption.class, () -> new SOPImpl().armor().label(ArmorLabel.Sig)); - } - - @Test - public void armor() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { - byte[] data = PGPainless.generateKeyRing().modernKeyRing("Alice", null).getEncoded(); - byte[] knownGoodArmor = ArmorUtils.toAsciiArmoredString(data).getBytes(StandardCharsets.UTF_8); + public void armor() throws IOException { + byte[] data = PGPainless.generateKeyRing().modernKeyRing("Alice").getEncoded(); + byte[] knownGoodArmor = ArmorUtils.toAsciiArmoredString(data) + .replace("Version: PGPainless\n", "") // armor command does not add version anymore + .getBytes(StandardCharsets.UTF_8); byte[] armored = new SOPImpl() .armor() .data(data) diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/CarolKeySignEncryptRoundtripTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/CarolKeySignEncryptRoundtripTest.java new file mode 100644 index 00000000..83778106 --- /dev/null +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/CarolKeySignEncryptRoundtripTest.java @@ -0,0 +1,298 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; +import sop.ByteArrayAndResult; +import sop.DecryptionResult; +import sop.EncryptionResult; +import sop.ReadyWithResult; +import sop.testsuite.assertions.VerificationListAssert; + +public class CarolKeySignEncryptRoundtripTest { + + private static final String CAROL_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "\n" + + "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\n" + + "1FkOSekLi8WNMdUx3XMyvP8nJ65P2Q==\n" + + "=Xj8h\n" + + "-----END PGP PRIVATE KEY BLOCK-----\n"; + + private static final String CAROL_CERT = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "\n" + + "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\n" + + "=pa/S\n" + + "-----END PGP PUBLIC KEY BLOCK-----\n"; + + private static final String BOB_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Comment: Bob's OpenPGP Transferable Secret Key\n" + + "\n" + + "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\n" + + "=miES\n" + + "-----END PGP PRIVATE KEY BLOCK-----\n"; + + private static final String BOB_CERT = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "Comment: Bob's OpenPGP certificate\n" + + "\n" + + "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\n" + + "NEJd3XZRzaXZE2aAMQ==\n" + + "=NXei\n" + + "-----END PGP PUBLIC KEY BLOCK-----\n"; + + @Test + public void regressionTest() throws IOException { + SOPImpl sop = new SOPImpl(); + byte[] msg = "Hello, World!\n".getBytes(); + ReadyWithResult encryption = sop.encrypt() + .signWith(CAROL_KEY.getBytes()) + .withCert(BOB_CERT.getBytes()) + .plaintext(msg); + byte[] ciphertext = encryption.toByteArrayAndResult().getBytes(); + + ByteArrayAndResult decryption = sop.decrypt() + .withKey(BOB_KEY.getBytes()) + .verifyWithCert(CAROL_CERT.getBytes()) + .ciphertext(ciphertext) + .toByteArrayAndResult(); + + byte[] plaintext = decryption.getBytes(); + assertArrayEquals(msg, plaintext); + VerificationListAssert.assertThatVerificationList(decryption.getResult().getVerifications()) + .hasSingleItem() + .issuedBy("71FFDA004409E5DDB0C3E8F19BA789DC76D6849A", "71FFDA004409E5DDB0C3E8F19BA789DC76D6849A"); + } +} diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/DetachInbandSignatureAndMessageTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/DetachInbandSignatureAndMessageTest.java deleted file mode 100644 index 06c11226..00000000 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/DetachInbandSignatureAndMessageTest.java +++ /dev/null @@ -1,78 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.sop; - -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.List; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.util.io.Streams; -import org.junit.jupiter.api.Test; -import org.pgpainless.PGPainless; -import org.pgpainless.algorithm.DocumentSignatureType; -import org.pgpainless.encryption_signing.EncryptionStream; -import org.pgpainless.encryption_signing.ProducerOptions; -import org.pgpainless.encryption_signing.SigningOptions; -import org.pgpainless.key.OpenPgpV4Fingerprint; -import org.pgpainless.key.protection.SecretKeyRingProtector; -import sop.ByteArrayAndResult; -import sop.SOP; -import sop.Signatures; -import sop.Verification; - -public class DetachInbandSignatureAndMessageTest { - - @Test - public void testDetachingOfInbandSignaturesAndMessage() throws IOException, PGPException { - SOP sop = new SOPImpl(); - byte[] key = sop.generateKey() - .userId("Alice ") - .generate() - .getBytes(); - byte[] cert = sop.extractCert().key(key).getBytes(); - PGPSecretKeyRing secretKey = PGPainless.readKeyRing().secretKeyRing(key); - - // Create a cleartext signed message - byte[] data = "Hello, World\n".getBytes(StandardCharsets.UTF_8); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - EncryptionStream signingStream = PGPainless.encryptAndOrSign() - .onOutputStream(out) - .withOptions( - ProducerOptions.sign( - SigningOptions.get() - .addDetachedSignature(SecretKeyRingProtector.unprotectedKeys(), - secretKey, DocumentSignatureType.BINARY_DOCUMENT) - ).setCleartextSigned()); - - Streams.pipeAll(new ByteArrayInputStream(data), signingStream); - signingStream.close(); - - // actually detach the message - ByteArrayAndResult detachedMsg = sop.detachInbandSignatureAndMessage() - .message(out.toByteArray()) - .toByteArrayAndResult(); - - byte[] message = detachedMsg.getBytes(); - byte[] signature = detachedMsg.getResult().getBytes(); - - List verificationList = sop.verify() - .cert(cert) - .signatures(signature) - .data(message); - - assertFalse(verificationList.isEmpty()); - assertEquals(1, verificationList.size()); - assertEquals(new OpenPgpV4Fingerprint(secretKey).toString(), verificationList.get(0).getSigningCertFingerprint()); - assertArrayEquals(data, message); - } -} diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/SignTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/DetachedSignTest.java similarity index 62% rename from pgpainless-sop/src/test/java/org/pgpainless/sop/SignTest.java rename to pgpainless-sop/src/test/java/org/pgpainless/sop/DetachedSignTest.java index 3167618c..7b2c9131 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/SignTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/DetachedSignTest.java @@ -11,27 +11,23 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.IOException; import java.nio.charset.StandardCharsets; -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; -import java.util.Arrays; import java.util.Date; import java.util.List; import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; import org.bouncycastle.openpgp.PGPSignature; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import org.pgpainless.PGPainless; import org.pgpainless.algorithm.SignatureType; import org.pgpainless.signature.SignatureUtils; import sop.SOP; import sop.Verification; import sop.enums.SignAs; +import sop.enums.SignatureMode; import sop.exception.SOPGPException; +import sop.testsuite.assertions.VerificationListAssert; -public class SignTest { +public class DetachedSignTest { private static SOP sop; private static byte[] key; @@ -55,19 +51,22 @@ public class SignTest { public void signArmored() throws IOException { byte[] signature = sop.sign() .key(key) + .mode(SignAs.binary) .data(data) - .getBytes(); + .toByteArrayAndResult().getBytes(); assertTrue(new String(signature).startsWith("-----BEGIN PGP SIGNATURE-----")); List verifications = sop.verify() .cert(cert) - .notAfter(new Date(new Date().getTime() + 10000)) - .notBefore(new Date(new Date().getTime() - 10000)) + .notAfter(new Date(System.currentTimeMillis() + 10000)) + .notBefore(new Date(System.currentTimeMillis() - 10000)) .signatures(signature) .data(data); - assertEquals(1, verifications.size()); + VerificationListAssert.assertThatVerificationList(verifications) + .hasSingleItem() + .hasMode(SignatureMode.binary); } @Test @@ -76,18 +75,40 @@ public class SignTest { .key(key) .noArmor() .data(data) - .getBytes(); + .toByteArrayAndResult().getBytes(); assertFalse(new String(signature).startsWith("-----BEGIN PGP SIGNATURE-----")); List verifications = sop.verify() .cert(cert) - .notAfter(new Date(new Date().getTime() + 10000)) - .notBefore(new Date(new Date().getTime() - 10000)) + .notAfter(new Date(System.currentTimeMillis() + 10000)) + .notBefore(new Date(System.currentTimeMillis() - 10000)) .signatures(signature) .data(data); - assertEquals(1, verifications.size()); + VerificationListAssert.assertThatVerificationList(verifications) + .hasSingleItem(); + } + + @Test + public void textSig() throws IOException { + byte[] signature = sop.sign() + .key(key) + .noArmor() + .mode(SignAs.text) + .data(data) + .toByteArrayAndResult().getBytes(); + + List verifications = sop.verify() + .cert(cert) + .notAfter(new Date(System.currentTimeMillis() + 10000)) + .notBefore(new Date(System.currentTimeMillis() - 10000)) + .signatures(signature) + .data(data); + + VerificationListAssert.assertThatVerificationList(verifications) + .hasSingleItem() + .hasMode(SignatureMode.text); } @Test @@ -95,11 +116,11 @@ public class SignTest { byte[] signature = sop.sign() .key(key) .data(data) - .getBytes(); + .toByteArrayAndResult().getBytes(); assertThrows(SOPGPException.NoSignature.class, () -> sop.verify() .cert(cert) - .notAfter(new Date(new Date().getTime() - 10000)) // Sig is older + .notAfter(new Date(System.currentTimeMillis() - 10000)) // Sig is older .signatures(signature) .data(data)); } @@ -109,11 +130,11 @@ public class SignTest { byte[] signature = sop.sign() .key(key) .data(data) - .getBytes(); + .toByteArrayAndResult().getBytes(); assertThrows(SOPGPException.NoSignature.class, () -> sop.verify() .cert(cert) - .notBefore(new Date(new Date().getTime() + 10000)) // Sig is younger + .notBefore(new Date(System.currentTimeMillis() + 10000)) // Sig is younger .signatures(signature) .data(data)); } @@ -121,32 +142,13 @@ public class SignTest { @Test public void mode() throws IOException, PGPException { byte[] signature = sop.sign() - .mode(SignAs.Text) + .mode(SignAs.text) .key(key) .data(data) - .getBytes(); + .toByteArrayAndResult().getBytes(); PGPSignature sig = SignatureUtils.readSignatures(signature).get(0); assertEquals(SignatureType.CANONICAL_TEXT_DOCUMENT.getCode(), sig.getSignatureType()); } - @Test - public void rejectKeyRingCollection() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { - PGPSecretKeyRing key1 = PGPainless.generateKeyRing().modernKeyRing("Alice", null); - PGPSecretKeyRing key2 = PGPainless.generateKeyRing().modernKeyRing("Bob", null); - PGPSecretKeyRingCollection collection = new PGPSecretKeyRingCollection(Arrays.asList(key1, key2)); - byte[] keys = collection.getEncoded(); - - assertThrows(SOPGPException.BadData.class, () -> sop.sign().key(keys)); - } - - @Test - public void rejectEncryptedKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { - PGPSecretKeyRing key = PGPainless.generateKeyRing() - .modernKeyRing("Alice", "passphrase"); - byte[] bytes = key.getEncoded(); - - assertThrows(SOPGPException.KeyIsProtected.class, () -> sop.sign().key(bytes)); - } - } diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/EncryptDecryptRoundTripTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/EncryptDecryptRoundTripTest.java index f6f6c235..aa366ef1 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/EncryptDecryptRoundTripTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/EncryptDecryptRoundTripTest.java @@ -7,9 +7,11 @@ package org.pgpainless.sop; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import org.bouncycastle.util.io.Streams; @@ -18,22 +20,28 @@ import org.junit.jupiter.api.Test; import sop.ByteArrayAndResult; import sop.DecryptionResult; import sop.SOP; +import sop.SessionKey; +import sop.enums.SignatureMode; import sop.exception.SOPGPException; +import sop.testsuite.assertions.VerificationListAssert; public class EncryptDecryptRoundTripTest { + private static final Charset utf8 = Charset.forName("UTF8"); private static SOP sop; private static byte[] aliceKey; + private static final String alicePassword = "wonderland.is.c00l"; private static byte[] aliceCert; private static byte[] bobKey; private static byte[] bobCert; - private static byte[] message = "Hello, World!\n".getBytes(StandardCharsets.UTF_8); + private static byte[] message = "Hello, World!\n".getBytes(utf8); @BeforeAll public static void setup() throws IOException { sop = new SOPImpl(); aliceKey = sop.generateKey() .userId("Alice ") + .withKeyPassword(alicePassword) .generate() .getBytes(); aliceCert = sop.extractCert() @@ -49,12 +57,14 @@ public class EncryptDecryptRoundTripTest { } @Test - public void basicRoundTripWithKey() throws IOException, SOPGPException.CertCannotSign { + public void basicRoundTripWithKey() throws IOException, SOPGPException.KeyCannotSign { byte[] encrypted = sop.encrypt() .signWith(aliceKey) + .withKeyPassword(alicePassword) .withCert(aliceCert) .withCert(bobCert) .plaintext(message) + .toByteArrayAndResult() .getBytes(); ByteArrayAndResult bytesAndResult = sop.decrypt() @@ -68,11 +78,12 @@ public class EncryptDecryptRoundTripTest { assertArrayEquals(message, decrypted.toByteArray()); DecryptionResult result = bytesAndResult.getResult(); - assertEquals(1, result.getVerifications().size()); + VerificationListAssert.assertThatVerificationList(result.getVerifications()) + .hasSingleItem(); } @Test - public void basicRoundTripWithoutArmorUsingKey() throws IOException, SOPGPException.CertCannotSign { + public void basicRoundTripWithoutArmorUsingKey() throws IOException, SOPGPException.KeyCannotSign { byte[] aliceKeyNoArmor = sop.generateKey() .userId("Alice ") .noArmor() @@ -87,6 +98,7 @@ public class EncryptDecryptRoundTripTest { .withCert(aliceCertNoArmor) .noArmor() .plaintext(message) + .toByteArrayAndResult() .getBytes(); ByteArrayAndResult bytesAndResult = sop.decrypt() @@ -99,7 +111,8 @@ public class EncryptDecryptRoundTripTest { assertArrayEquals(message, decrypted); DecryptionResult result = bytesAndResult.getResult(); - assertEquals(1, result.getVerifications().size()); + VerificationListAssert.assertThatVerificationList(result.getVerifications()) + .hasSingleItem(); } @Test @@ -107,6 +120,7 @@ public class EncryptDecryptRoundTripTest { byte[] encrypted = sop.encrypt() .withPassword("passphr4s3") .plaintext(message) + .toByteArrayAndResult() .getBytes(); ByteArrayAndResult bytesAndResult = sop.decrypt() @@ -118,7 +132,8 @@ public class EncryptDecryptRoundTripTest { assertArrayEquals(message, decrypted); DecryptionResult result = bytesAndResult.getResult(); - assertEquals(0, result.getVerifications().size()); + VerificationListAssert.assertThatVerificationList(result.getVerifications()) + .isEmpty(); } @Test @@ -129,6 +144,7 @@ public class EncryptDecryptRoundTripTest { sop.encrypt() .withPassword("passphr4s3") .plaintext(message) + .toByteArrayAndResult() .getInputStream() ) .toByteArrayAndResult(); @@ -137,7 +153,8 @@ public class EncryptDecryptRoundTripTest { assertArrayEquals(message, decrypted); DecryptionResult result = bytesAndResult.getResult(); - assertEquals(0, result.getVerifications().size()); + VerificationListAssert.assertThatVerificationList(result.getVerifications()) + .isEmpty(); } @Test @@ -145,6 +162,7 @@ public class EncryptDecryptRoundTripTest { byte[] encrypted = sop.encrypt() .withPassword("passphr4s3 ") .plaintext(message) + .toByteArrayAndResult() .getBytes(); ByteArrayAndResult bytesAndResult = sop.decrypt() @@ -156,22 +174,27 @@ public class EncryptDecryptRoundTripTest { assertArrayEquals(message, decrypted); DecryptionResult result = bytesAndResult.getResult(); - assertEquals(0, result.getVerifications().size()); + VerificationListAssert.assertThatVerificationList(result.getVerifications()) + .isEmpty(); } @Test - public void encrypt_decryptAndVerifyYieldsNoSignatureException() throws IOException { + public void encrypt_decryptAndVerifyYieldsNoVerifications() throws IOException { byte[] encrypted = sop.encrypt() .withCert(bobCert) .plaintext(message) + .toByteArrayAndResult() .getBytes(); - assertThrows(SOPGPException.NoSignature.class, () -> sop - .decrypt() + DecryptionResult result = sop.decrypt() .withKey(bobKey) .verifyWithCert(aliceCert) .ciphertext(encrypted) - .toByteArrayAndResult()); + .toByteArrayAndResult() + .getResult(); + + VerificationListAssert.assertThatVerificationList(result.getVerifications()) + .isEmpty(); } @Test @@ -179,6 +202,7 @@ public class EncryptDecryptRoundTripTest { byte[] encrypted = sop.encrypt() .withCert(bobCert) .plaintext(message) + .toByteArrayAndResult() .getBytes(); assertThrows(SOPGPException.MissingArg.class, () -> sop @@ -187,16 +211,6 @@ public class EncryptDecryptRoundTripTest { .toByteArrayAndResult()); } - @Test - public void decrypt_withKeyWithMultipleKeysFails() { - byte[] keys = new byte[aliceKey.length + bobKey.length]; - System.arraycopy(aliceKey, 0, keys, 0 , aliceKey.length); - System.arraycopy(bobKey, 0, keys, aliceKey.length, bobKey.length); - - assertThrows(SOPGPException.BadData.class, () -> sop.decrypt() - .withKey(keys)); - } - @Test public void decrypt_withKeyWithPasswordProtectionFails() { String passwordProtectedKey = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + @@ -226,8 +240,122 @@ public class EncryptDecryptRoundTripTest { "=MUYS\n" + "-----END PGP PRIVATE KEY BLOCK-----"; + String msg = "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "hF4Doj0CaB2GRvISAQdAhV5sjUCxanM68jG9qaq2rep1KKQx2o+9yrK0Rsrtqkww\n" + + "mb4uVv/SD3ixDztUSgUset0jeUeZHZAWfTB9cWawX4fiB2BdbcxhxFqQR8VPJ2SZ\n" + + "0jcB+wH1gq05AkMaCfoEIio3o3QcZq2In8tqj69U3AFRQApoH/p+ZLDz2pcnFBn+\n" + + "x1Y+C6wNg/3g\n" + + "=6vge\n" + + "-----END PGP MESSAGE-----"; + assertThrows(SOPGPException.KeyIsProtected.class, () -> sop.decrypt() - .withKey(passwordProtectedKey.getBytes(StandardCharsets.UTF_8))); + .withKey(passwordProtectedKey.getBytes(StandardCharsets.UTF_8)) + .ciphertext(msg.getBytes(utf8))); + } + + @Test + public void encryptDecryptRoundTripWithProtectedKey() throws IOException { + byte[] passphrase = "sw0rdf1sh".getBytes(utf8); + + byte[] key = sop.generateKey() + .userId("Alice ") + .withKeyPassword(passphrase) + .generate().getBytes(); + + byte[] cert = sop.extractCert() + .key(key) + .getBytes(); + + byte[] plaintext = "Hello, World!\n".getBytes(utf8); + + byte[] ciphertext = sop.encrypt() + .withCert(cert) + .plaintext(plaintext) + .toByteArrayAndResult() + .getBytes(); + + byte[] decrypted = sop.decrypt() + .withKeyPassword(passphrase) + .withKey(key) + .ciphertext(ciphertext) + .toByteArrayAndResult() + .getBytes(); + + assertArrayEquals(plaintext, decrypted); + } + + @Test + public void encryptDecryptRoundTripWithTwoProtectedKeysAndOnePassphrase() throws IOException { + byte[] passphrase1 = "sw0rdf1sh".getBytes(utf8); + + byte[] key1 = sop.generateKey() + .userId("Alice ") + .withKeyPassword(passphrase1) + .generate().getBytes(); + + byte[] cert1 = sop.extractCert() + .key(key1) + .getBytes(); + + byte[] passphrase2 = "fooBar".getBytes(utf8); + + byte[] key2 = sop.generateKey() + .userId("Bob ") + .withKeyPassword(passphrase2) + .generate().getBytes(); + + byte[] cert2 = sop.extractCert() + .key(key2) + .getBytes(); + + byte[] plaintext = "Hello, World!\n".getBytes(utf8); + + byte[] ciphertext = sop.encrypt() + .withCert(cert1) + .withCert(cert2) + .plaintext(plaintext) + .toByteArrayAndResult() + .getBytes(); + + byte[] decrypted = sop.decrypt() + .withKey(key1) + .withKey(key2) + .withKeyPassword(passphrase2) + .ciphertext(ciphertext) + .toByteArrayAndResult() + .getBytes(); + + assertArrayEquals(plaintext, decrypted); + } + + @Test + public void encryptDecryptRoundTripFailsWithProtectedKeyAndWrongPassphrase() throws IOException { + byte[] passphrase = "sw0rdf1sh".getBytes(utf8); + + byte[] key = sop.generateKey() + .userId("Alice ") + .withKeyPassword(passphrase) + .generate().getBytes(); + + byte[] cert = sop.extractCert() + .key(key) + .getBytes(); + + byte[] plaintext = "Hello, World!\n".getBytes(utf8); + + byte[] ciphertext = sop.encrypt() + .withCert(cert) + .plaintext(plaintext) + .toByteArrayAndResult() + .getBytes(); + + assertThrows(SOPGPException.KeyIsProtected.class, + () -> sop.decrypt() + .withKeyPassword("foobar") + .withKey(key) + .ciphertext(ciphertext)); } @Test @@ -235,4 +363,237 @@ public class EncryptDecryptRoundTripTest { assertThrows(SOPGPException.BadData.class, () -> sop.decrypt() .verifyWithCert(new byte[0])); } + + @Test + public void testPassphraseDecryptionYieldsSessionKey() throws IOException { + byte[] message = "Hello\nWorld\n".getBytes(StandardCharsets.UTF_8); + byte[] ciphertext = ("-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "jA0ECQMCdFswArqHpj1g0j4BLDTkZhCC1crZf0EFq1xPIMUtnyRmfJJ7IzsdMJ5Y\n" + + "EhKbBc2h6wIX7B/GxUbyNj1xh5JRzt2ZX8KL2d6HAQ==\n" + + "=zZ0/\n" + + "-----END PGP MESSAGE-----").getBytes(StandardCharsets.UTF_8); + String passphrase = "sw0rdf1sh"; + ByteArrayAndResult bytesAndResult = sop.decrypt().withPassword(passphrase).ciphertext(ciphertext).toByteArrayAndResult(); + assertArrayEquals(message, bytesAndResult.getBytes()); + assertTrue(bytesAndResult.getResult().getSessionKey().isPresent()); + assertEquals("9:7BCB7383D23E20D4BA8980B26D6C0813769056546C45B7E55F4612BFAD5B4B1C", bytesAndResult.getResult().getSessionKey().get().toString()); + } + + @Test + public void testPublicKeyDecryptionYieldsSessionKey() throws IOException { + byte[] key = ("-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: D94A AA9C 5F73 48B2 5D81 72E7 F20C F71E 93FE 897F\n" + + "Comment: Alice\n" + + "\n" + + "lFgEYWlyaRYJKwYBBAHaRw8BAQdAJsJfjByLE+8HNVGbEKiIbSGXBYwR6L61bT5E\n" + + "Hhu642kAAP49D4TOaI+Z3G5ko4C4D1bOzLajLRpIuPLuwYHpF1xD0RHmtAVBbGlj\n" + + "ZYh4BBMWCgAgBQJhaXJpAhsBBRYCAwEABRUKCQgLBAsJCAcCHgECGQEACgkQ8gz3\n" + + "HpP+iX/c8AD9Hx0PUu97n8ZlrpuA6YuJL3rONPQnaXMz9eE+KHxJS6sBAM06X8Wm\n" + + "XRGUVURsoerwYTbUnXcUnqH/U/JhwlUerJAInF0EYWlyaRIKKwYBBAGXVQEFAQEH\n" + + "QJOHyxI5K8ZqX+v/AmTLHAIjWd8wHO8eGld4KHniCFx9AwEIBwAA/0zVZYYWsr3w\n" + + "GKkmqfIZlB+wIeJlWrho87kvXiNAe0LIEIGIdQQYFgoAHQUCYWlyaQIbDAUWAgMB\n" + + "AAUVCgkICwQLCQgHAh4BAAoJEPIM9x6T/ol/vggA/ilxi5UTjDYDR7sGrYyaGPRK\n" + + "Sg0KNn2SV4c5M5ZmZR7sAP4kKz6kQ4UtYmSmUmMBu+A3mMTN8VQY+6LSTdekvU0N\n" + + "ApxYBGFpcmkWCSsGAQQB2kcPAQEHQJiiZENQ52jyt8wBwX7fD1vQkvgTg5T3v1S1\n" + + "yzr1yI0RAAD+KOTcMdv8rz3U6K42PNE4b983KoMfyQ/hgjIWOi2BYBwP94jVBBgW\n" + + "CgB9BQJhaXJpAhsCBRYCAwEABRUKCQgLBAsJCAcCHgFfIAQZFgoABgUCYWlyaQAK\n" + + "CRDP7lemqmadIYLuAP9oAm+OFzyMNrmWRcvdHqH/DAfJTM2+ZmANSm44geZDEAD9\n" + + "HfeCHev1H1H1wOd0S3tW9gZwonrYFoqOBW/YTmf5XwYACgkQ8gz3HpP+iX+veQEA\n" + + "sWC+xDo+lc6oJr4q0mTJkxzYfgUBtQ0VjUWNcGyOdegBAL8hMzb9+e4wbP2F0tMb\n" + + "ZFA2MgHsvqGhXyAXi50arZYF\n" + + "=k66N\n" + + "-----END PGP PRIVATE KEY BLOCK-----\n").getBytes(StandardCharsets.UTF_8); + byte[] message = "Hello\nWorld\n".getBytes(StandardCharsets.UTF_8); + byte[] ciphertext = ("-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "hF4DrJ3c2YF1IKUSAQdA9VL6OwIwOwB4GnE4yR5JJ5OjcC76WTpdm85I6WHvhD4w\n" + + "hqHpf6UGaDDQ7xAcSd7YnEGVMBOOBnJfD1PRuNWE5hwgqqsqpMDrvvMHjUsg3HNH\n" + + "0j4BriMU8XQ6MLdvCaFmeQqFwBD4mlI/x32wj0I9VyBIKysopA8HNV4ES2rOhGuW\n" + + "T/zFmI9Tm9eWvNwv0LUNhQ==\n" + + "=4Z+m\n" + + "-----END PGP MESSAGE-----\n").getBytes(StandardCharsets.UTF_8); + + ByteArrayAndResult bytesAndResult = sop.decrypt().withKey(key).ciphertext(ciphertext).toByteArrayAndResult(); + DecryptionResult result = bytesAndResult.getResult(); + assertArrayEquals(message, bytesAndResult.getBytes()); + assertTrue(result.getSessionKey().isPresent()); + assertEquals("9:63F741E7FB60247BE59C64158573308F727236482DB7653908C95839E4166AAE", result.getSessionKey().get().toString()); + } + + @Test + public void testDecryptionWithSessionKey() throws IOException { + byte[] message = "Hello\nWorld\n".getBytes(StandardCharsets.UTF_8); + byte[] ciphertext = ("-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "hF4DrJ3c2YF1IKUSAQdA9VL6OwIwOwB4GnE4yR5JJ5OjcC76WTpdm85I6WHvhD4w\n" + + "hqHpf6UGaDDQ7xAcSd7YnEGVMBOOBnJfD1PRuNWE5hwgqqsqpMDrvvMHjUsg3HNH\n" + + "0j4BriMU8XQ6MLdvCaFmeQqFwBD4mlI/x32wj0I9VyBIKysopA8HNV4ES2rOhGuW\n" + + "T/zFmI9Tm9eWvNwv0LUNhQ==\n" + + "=4Z+m\n" + + "-----END PGP MESSAGE-----\n").getBytes(StandardCharsets.UTF_8); + SessionKey sessionKey = SessionKey.fromString("9:63F741E7FB60247BE59C64158573308F727236482DB7653908C95839E4166AAE"); + + ByteArrayAndResult bytesAndResult = sop.decrypt().withSessionKey(sessionKey) + .ciphertext(ciphertext) + .toByteArrayAndResult(); + + DecryptionResult result = bytesAndResult.getResult(); + assertTrue(result.getSessionKey().isPresent()); + assertEquals(sessionKey, result.getSessionKey().get()); + + assertArrayEquals(message, bytesAndResult.getBytes()); + } + + @Test + public void testEncryptWithWrongPassphraseThrowsKeyIsProtected() { + assertThrows(SOPGPException.KeyIsProtected.class, () -> sop.encrypt() + .withKeyPassword("falsePassphrase") + .signWith(aliceKey) + .withCert(bobCert) + .plaintext(message)); + } + + @Test + public void testDecryptionWithSessionKey_VerificationWithCert() throws IOException { + byte[] plaintext = "This is a test message.\nSit back and relax.\n".getBytes(StandardCharsets.UTF_8); + byte[] key = ("-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 9C26 EFAB 1C65 00A2 28E8 A9C2 658E E420 C824 D191\n" + + "Comment: Alice\n" + + "\n" + + "lFgEYWl4ixYJKwYBBAHaRw8BAQdAv6+cd8R/ICS/z9hlT99g++wyquxVsO0FCb8F\n" + + "MSkTplUAAP9gPoBi8fxdfLaEyt6GWeIBTeYsVxsogbKzXXnjp3MbiRE/tAVBbGlj\n" + + "ZYh4BBMWCgAgBQJhaXiLAhsBBRYCAwEABRUKCQgLBAsJCAcCHgECGQEACgkQZY7k\n" + + "IMgk0ZEZuAEA3hWzfqCXGUjlv+miWey1AyWRu9eQvTdE9YqbIMuxIk4BAMtGlo6l\n" + + "d3E868q0zLOOktmsBxnzaE7knbd9nAlK3FUJnF0EYWl4ixIKKwYBBAGXVQEFAQEH\n" + + "QK8vS3T3Yf3Gpy9iWOTR0jdhV4XgtchcvKCpFMgc5uwFAwEIBwAA/1tNle5cT9kS\n" + + "8yzNxL16ElEREtEX+5kpkt6JZyTx0xfAEPGIdQQYFgoAHQUCYWl4iwIbDAUWAgMB\n" + + "AAUVCgkICwQLCQgHAh4BAAoJEGWO5CDIJNGRM80BANJ6EGKIkVNxYj7wOaEqyRh1\n" + + "Rtv3tLAnEzLl/b0mZx3WAQDADAPNCl5xnjTt5InyfrwV90kM4vDGcl4mQE8FD7dD\n" + + "B5xYBGFpeIsWCSsGAQQB2kcPAQEHQFuEaBKUllw+MfdkkSNE0CncJCeFGCbHvmsc\n" + + "Ma/DPgrpAAEAlsoxcTyTFfHxV2CayDCFvBSHYXOSOg6fyMdh0SxzjC0PVIjVBBgW\n" + + "CgB9BQJhaXiLAhsCBRYCAwEABRUKCQgLBAsJCAcCHgFfIAQZFgoABgUCYWl4iwAK\n" + + "CRBGMq3j1oKUXenjAP974AvBOAVIdNUkVAishoDL7ee7/eAU3Ni7V2Kn47cusQD/\n" + + "c8c9phtf2NIL23K4bvBdvsU3opV2DIVJwRutV4v6jgAACgkQZY7kIMgk0ZG1dwEA\n" + + "sFp1AuPcn3dGF05D6ohlqunoBwBWEcwZLjx+v5X27R8A/17V5nzC+eny3XjCF8Ib\n" + + "qw1VTfR84stki65Xhm2lxFAN\n" + + "=TQO7\n" + + "-----END PGP PRIVATE KEY BLOCK-----\n").getBytes(StandardCharsets.UTF_8); + byte[] cert = sop.extractCert().key(key).getBytes(); + byte[] ciphertext = ("-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "hF4DSjXDMRql2RASAQdAkhJyA9GX5ios8PNlti7v7BieggiiR9trqrKFQwomU2Aw\n" + + "elEFuDA3ugJO2rNiyQQH1riFFJuod6BiQuxFhdf/mmsFFDzHmJeUOx9pQeNemzST\n" + + "0sAdAQQYC+iXUNn2y15kTqbFQFgfOWObgsqspGY04V17fZdVI7bEORLM+YT6KoZA\n" + + "uq2WO49ze9jp2jdvTsjjNNseZDhmxtgOCfi1/Fi3IHPnBJW7M3UWaJCSLozWkO95\n" + + "FztCSWL22jDGPGIjgQ589hYW+WuJMvMv6ltTOo+l70S5dHSObijbcOqfNSmrxlpw\n" + + "hqZfkU0BA01I9Pf3lBPCNyMbCPZP0oaIiWACnm6svWp4oH5u5ClhS9BVJTptzwXv\n" + + "mMj+lTi5ahGQJ3Nr8krloTSsjpkssz6D2+FDnvjwu6E=\n" + + "=BYOB\n" + + "-----END PGP MESSAGE-----").getBytes(StandardCharsets.UTF_8); + String sessionKey = "9:87C0870598AD908ABEECCAE265DCEEA146CF557AAF698D097024404A00EBD072"; + + // Decrypt with public key + ByteArrayAndResult bytesAndResult = + sop.decrypt().withKey(key).verifyWithCert(cert).ciphertext(ciphertext).toByteArrayAndResult(); + assertEquals(sessionKey, bytesAndResult.getResult().getSessionKey().get().toString()); + assertArrayEquals(plaintext, bytesAndResult.getBytes()); + VerificationListAssert.assertThatVerificationList(bytesAndResult.getResult().getVerifications()) + .hasSingleItem() + .issuedBy("9C26EFAB1C6500A228E8A9C2658EE420C824D191") + .hasMode(SignatureMode.binary); + // Decrypt with session key + bytesAndResult = sop.decrypt().withSessionKey(SessionKey.fromString(sessionKey)) + .verifyWithCert(cert).ciphertext(ciphertext).toByteArrayAndResult(); + assertEquals(sessionKey, bytesAndResult.getResult().getSessionKey().get().toString()); + assertArrayEquals(plaintext, bytesAndResult.getBytes()); + VerificationListAssert.assertThatVerificationList(bytesAndResult.getResult().getVerifications()) + .hasSingleItem() + .issuedBy("9C26EFAB1C6500A228E8A9C2658EE420C824D191") + .hasMode(SignatureMode.binary); + } + + @Test + public void decryptWithWrongSessionKey() { + byte[] ciphertext = ("-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "hF4DSjXDMRql2RASAQdAkhJyA9GX5ios8PNlti7v7BieggiiR9trqrKFQwomU2Aw\n" + + "elEFuDA3ugJO2rNiyQQH1riFFJuod6BiQuxFhdf/mmsFFDzHmJeUOx9pQeNemzST\n" + + "0sAdAQQYC+iXUNn2y15kTqbFQFgfOWObgsqspGY04V17fZdVI7bEORLM+YT6KoZA\n" + + "uq2WO49ze9jp2jdvTsjjNNseZDhmxtgOCfi1/Fi3IHPnBJW7M3UWaJCSLozWkO95\n" + + "FztCSWL22jDGPGIjgQ589hYW+WuJMvMv6ltTOo+l70S5dHSObijbcOqfNSmrxlpw\n" + + "hqZfkU0BA01I9Pf3lBPCNyMbCPZP0oaIiWACnm6svWp4oH5u5ClhS9BVJTptzwXv\n" + + "mMj+lTi5ahGQJ3Nr8krloTSsjpkssz6D2+FDnvjwu6E=\n" + + "=BYOB\n" + + "-----END PGP MESSAGE-----").getBytes(StandardCharsets.UTF_8); + SessionKey wrongSessionKey = SessionKey.fromString("9:63F741E7FB60247BE59C64158573308F727236482DB7653908C95839E4166AAE"); + + assertThrows(SOPGPException.CannotDecrypt.class, () -> + sop.decrypt().withSessionKey(wrongSessionKey).ciphertext(ciphertext)); + } + + @Test + public void decryptNonEncryptedDataFailsBadData() throws IOException { + byte[] signed = sop.inlineSign() + .key(aliceKey) + .withKeyPassword(alicePassword) + .data(message) + .getBytes(); + + assertThrows(SOPGPException.BadData.class, () -> + sop.decrypt() + .verifyWithCert(aliceCert) + .withKey(aliceKey) + .withKeyPassword(alicePassword) + .ciphertext(signed) + .toByteArrayAndResult() + ); + } + + @Test + public void encryptWithUnsupportedProfileFails() { + assertThrows(SOPGPException.UnsupportedProfile.class, () -> sop + .encrypt() + .profile("Unknown")); + } + + @Test + public void encryptWithSupportedProfileTest() throws IOException { + + byte[] key = sop.generateKey() + .profile("rfc4880") + .userId("Alice ") + .generate() + .getBytes(); + + byte[] cert = sop.extractCert() + .key(key) + .getBytes(); + + byte[] encrypted = sop.encrypt() + .profile("rfc4880") + .withCert(cert) + .plaintext(message) + .toByteArrayAndResult() + .getBytes(); + + ByteArrayAndResult bytesAndResult = sop.decrypt() + .withKey(key) + .ciphertext(encrypted) + .toByteArrayAndResult(); + + ByteArrayOutputStream decrypted = new ByteArrayOutputStream(); + Streams.pipeAll(bytesAndResult.getInputStream(), decrypted); + assertArrayEquals(message, decrypted.toByteArray()); + + DecryptionResult result = bytesAndResult.getResult(); + assertTrue(result.getSessionKey().isPresent()); + } } diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/ExtractCertTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/ExtractCertTest.java index 84a1f471..b3910482 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/ExtractCertTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/ExtractCertTest.java @@ -18,7 +18,6 @@ import sop.exception.SOPGPException; public class ExtractCertTest { public static final String key = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + - "Version: PGPainless\n" + "Comment: A8D9 9FF4 C8DD BBA6 C610 A6B7 9ACB 2195 A9BC DF5B\n" + "Comment: Alice \n" + "\n" + @@ -42,7 +41,6 @@ public class ExtractCertTest { "-----END PGP PRIVATE KEY BLOCK-----\n"; public static final String cert = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + - "Version: PGPainless\n" + "Comment: A8D9 9FF4 C8DD BBA6 C610 A6B7 9ACB 2195 A9BC DF5B\n" + "Comment: Alice \n" + "\n" + diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/GenerateKeyTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/GenerateKeyTest.java index 7f1710fd..521cdfe0 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/GenerateKeyTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/GenerateKeyTest.java @@ -6,16 +6,21 @@ package org.pgpainless.sop; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.IOException; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.key.info.KeyRingInfo; +import org.pgpainless.key.protection.UnlockSecretKey; +import org.pgpainless.util.Passphrase; import sop.SOP; import sop.exception.SOPGPException; @@ -28,11 +33,6 @@ public class GenerateKeyTest { sop = new SOPImpl(); } - @Test - public void testMissingUserId() { - assertThrows(SOPGPException.MissingArg.class, () -> sop.generateKey().generate()); - } - @Test public void generateKey() throws IOException { byte[] bytes = sop.generateKey() @@ -74,4 +74,40 @@ public class GenerateKeyTest { assertFalse(new String(bytes).startsWith("-----BEGIN PGP PRIVATE KEY BLOCK-----")); } + + @Test + public void protectedMultiUserIdKey() throws IOException, PGPException { + byte[] bytes = sop.generateKey() + .userId("Alice") + .userId("Bob") + .withKeyPassword("sw0rdf1sh") + .generate() + .getBytes(); + + PGPSecretKeyRing secretKey = PGPainless.readKeyRing().secretKeyRing(bytes); + KeyRingInfo info = PGPainless.inspectKeyRing(secretKey); + + assertTrue(info.getUserIds().contains("Alice")); + assertTrue(info.getUserIds().contains("Bob")); + + for (PGPSecretKey key : secretKey) { + assertNotNull(UnlockSecretKey.unlockSecretKey(key, Passphrase.fromPassword("sw0rdf1sh"))); + } + } + + @Test + public void invalidProfile() { + assertThrows(SOPGPException.UnsupportedProfile.class, () -> + sop.generateKey().profile("invalid")); + } + + @Test + public void generateKeyWithNewlinesInUserId() throws IOException { + byte[] keyBytes = sop.generateKey() + .userId("Foo\n\nBar") + .generate() + .getBytes(); + + assertTrue(new String(keyBytes).contains("Foo\\n\\nBar")); + } } diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/IncapableKeysTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/IncapableKeysTest.java new file mode 100644 index 00000000..efcd51c4 --- /dev/null +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/IncapableKeysTest.java @@ -0,0 +1,72 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.KeyFlag; +import org.pgpainless.key.generation.KeySpec; +import org.pgpainless.key.generation.type.KeyType; +import org.pgpainless.key.generation.type.ecc.EllipticCurve; +import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve; +import org.pgpainless.util.ArmorUtils; +import sop.SOP; +import sop.exception.SOPGPException; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class IncapableKeysTest { + + private static byte[] nonSigningKey; + private static byte[] nonEncryptionKey; + private static byte[] nonSigningCert; + private static byte[] nonEncryptionCert; + + private static final SOP sop = new SOPImpl(); + + @BeforeAll + public static void generateKeys() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + PGPSecretKeyRing key = PGPainless.buildKeyRing() + .addSubkey(KeySpec.getBuilder(KeyType.ECDH(EllipticCurve._P256), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)) + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER)) + .addUserId("Non Signing ") + .build(); + nonSigningKey = ArmorUtils.toAsciiArmoredString(key).getBytes(StandardCharsets.UTF_8); + nonSigningCert = sop.extractCert().key(nonSigningKey).getBytes(); + + key = PGPainless.buildKeyRing() + .addSubkey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.SIGN_DATA)) + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER)) + .addUserId("Non Encryption ") + .build(); + nonEncryptionKey = ArmorUtils.toAsciiArmoredString(key).getBytes(StandardCharsets.UTF_8); + nonEncryptionCert = sop.extractCert().key(nonEncryptionKey).getBytes(); + } + + @Test + public void encryptionToNonEncryptionKeyFails() { + assertThrows(SOPGPException.CertCannotEncrypt.class, () -> sop.encrypt().withCert(nonEncryptionCert)); + } + + @Test + public void signingWithNonSigningKeyFails() { + assertThrows(SOPGPException.KeyCannotSign.class, () -> sop.sign().key(nonSigningKey)); + assertThrows(SOPGPException.KeyCannotSign.class, () -> sop.detachedSign().key(nonSigningKey)); + assertThrows(SOPGPException.KeyCannotSign.class, () -> sop.inlineSign().key(nonSigningKey)); + } + + @Test + public void encryptAndSignWithNonSigningKeyFails() { + assertThrows(SOPGPException.KeyCannotSign.class, () -> sop.encrypt().signWith(nonSigningKey)); + } +} diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/InlineDetachTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/InlineDetachTest.java new file mode 100644 index 00000000..98279e4f --- /dev/null +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/InlineDetachTest.java @@ -0,0 +1,204 @@ +// SPDX-FileCopyrightText: 2021 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.List; + +import org.bouncycastle.bcpg.ArmoredInputStream; +import org.bouncycastle.openpgp.PGPCompressedData; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPLiteralData; +import org.bouncycastle.openpgp.PGPLiteralDataGenerator; +import org.bouncycastle.openpgp.PGPObjectFactory; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.PGPSignatureList; +import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.implementation.ImplementationFactory; +import org.pgpainless.key.OpenPgpV4Fingerprint; +import sop.ByteArrayAndResult; +import sop.SOP; +import sop.Signatures; +import sop.Verification; +import sop.enums.InlineSignAs; +import sop.enums.SignatureMode; +import sop.exception.SOPGPException; +import sop.testsuite.assertions.VerificationListAssert; + +public class InlineDetachTest { + + private static final SOP sop = new SOPImpl(); + + /** + * Construct a message which is signed using the cleartext signature framework. + * The message consists of an armor header followed by the dash-escaped message data, followed by an armored signature. + * + * Detaching must result in the unescaped message data plus the signature packet. + * Verifying the signature must work. + * + * @throws IOException in case of an IO error + */ + @Test + public void detachCleartextSignedMessage() throws IOException { + byte[] key = sop.generateKey() + .userId("Alice ") + .generate() + .getBytes(); + byte[] cert = sop.extractCert().key(key).getBytes(); + PGPSecretKeyRing secretKey = PGPainless.readKeyRing().secretKeyRing(key); + + // Create a cleartext signed message + byte[] data = "Hello, World\n".getBytes(StandardCharsets.UTF_8); + byte[] cleartextSigned = sop.inlineSign() + .key(key) + .withKeyPassword("sw0rdf1sh") + .mode(InlineSignAs.clearsigned) + .data(data).getBytes(); + + // actually detach the message + ByteArrayAndResult detachedMsg = sop.inlineDetach() + .message(cleartextSigned) + .toByteArrayAndResult(); + + byte[] message = detachedMsg.getBytes(); + byte[] signature = detachedMsg.getResult().getBytes(); + + List verificationList = sop.verify() + .cert(cert) + .signatures(signature) + .data(message); + + VerificationListAssert.assertThatVerificationList(verificationList) + .hasSingleItem() + .issuedBy(new OpenPgpV4Fingerprint(secretKey).toString()) + .hasMode(SignatureMode.text); + + assertArrayEquals(data, message); + } + + /** + * Construct a message which is inline-signed. + * The message consists of a compressed data packet containing an OnePassSignature, a literal data packet and + * a signature packet. + * + * Detaching the message must result in the contents of the literal data packet, plus the signature packet. + * Verification must work. + * + * @throws IOException in case of an IO error + */ + @Test + public void detachInbandSignedMessage() throws IOException { + byte[] key = sop.generateKey() + .userId("Alice ") + .generate() + .getBytes(); + byte[] cert = sop.extractCert().key(key).getBytes(); + + byte[] data = "Hello, World\n".getBytes(StandardCharsets.UTF_8); + byte[] inlineSigned = sop.inlineSign() + .key(key) + .data(data).getBytes(); + + // actually detach the message + ByteArrayAndResult detachedMsg = sop.inlineDetach() + .message(inlineSigned) + .toByteArrayAndResult(); + + byte[] message = detachedMsg.getBytes(); + byte[] signature = detachedMsg.getResult().getBytes(); + + List verificationList = sop.verify() + .cert(cert) + .signatures(signature) + .data(message); + + VerificationListAssert.assertThatVerificationList(verificationList) + .hasSingleItem() + .hasMode(SignatureMode.binary); + + assertArrayEquals(data, message); + } + + /** + * Construct a message which consists of a literal data packet followed by a signatures block. + * Detaching it must result in the contents of the literal data packet plus the signatures block. + * + * Verification must still work. + * + * @throws IOException in case of an IO error + */ + @Test + public void detachOpenPgpMessage() throws IOException { + byte[] key = sop.generateKey() + .userId("Alice ") + .generate() + .getBytes(); + byte[] cert = sop.extractCert().key(key).getBytes(); + + byte[] data = "Hello, World\n".getBytes(StandardCharsets.UTF_8); + byte[] inlineSigned = sop.inlineSign() + .key(key) + .data(data).getBytes(); + + ByteArrayOutputStream literalDataAndSignatures = new ByteArrayOutputStream(); + ArmoredInputStream armorIn = new ArmoredInputStream(new ByteArrayInputStream(inlineSigned)); + PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(armorIn); + Object next; + while ((next = objectFactory.nextObject()) != null) { + if (next instanceof PGPCompressedData) { + PGPCompressedData compressedData = (PGPCompressedData) next; + try { + objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(compressedData.getDataStream()); + } catch (PGPException e) { + throw new SOPGPException.BadData("Cannot decompress compressed data", e); + } + continue; + } + if (next instanceof PGPLiteralData) { + PGPLiteralData litDat = (PGPLiteralData) next; + PGPLiteralDataGenerator litGen = new PGPLiteralDataGenerator(); + OutputStream litOut = litGen.open(literalDataAndSignatures, (char) litDat.getFormat(), litDat.getFileName(), litDat.getModificationTime(), new byte[8192]); + Streams.pipeAll(litDat.getDataStream(), litOut); + litOut.close(); + continue; + } + + if (next instanceof PGPSignatureList) { + PGPSignatureList signatures = (PGPSignatureList) next; + for (PGPSignature signature : signatures) { + signature.encode(literalDataAndSignatures); + } + } + } + + // actually detach the message + ByteArrayAndResult detachedMsg = sop.inlineDetach() + .message(literalDataAndSignatures.toByteArray()) + .toByteArrayAndResult(); + + byte[] message = detachedMsg.getBytes(); + byte[] signature = detachedMsg.getResult().getBytes(); + + List verificationList = sop.verify() + .cert(cert) + .signatures(signature) + .data(message); + + VerificationListAssert.assertThatVerificationList(verificationList) + .hasSingleItem() + .hasMode(SignatureMode.binary); + + assertArrayEquals(data, message); + } +} diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/InlineSignVerifyRoundtripTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/InlineSignVerifyRoundtripTest.java new file mode 100644 index 00000000..f3a50fc3 --- /dev/null +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/InlineSignVerifyRoundtripTest.java @@ -0,0 +1,125 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.List; + +import org.junit.jupiter.api.Test; +import sop.ByteArrayAndResult; +import sop.SOP; +import sop.Verification; +import sop.enums.InlineSignAs; +import sop.enums.SignatureMode; +import sop.testsuite.assertions.VerificationListAssert; + +public class InlineSignVerifyRoundtripTest { + + private static final SOP sop = new SOPImpl(); + + @Test + public void testInlineSignAndVerifyWithCleartextSignatures() throws IOException { + byte[] key = sop.generateKey() + .userId("Werner") + .withKeyPassword("sw0rdf1sh") + .generate().getBytes(); + + byte[] cert = sop.extractCert() + .key(key).getBytes(); + + byte[] message = "If you want something different, create a new protocol but don't try to\npush it onto a working system.\n".getBytes(StandardCharsets.UTF_8); + + byte[] inlineSigned = sop.inlineSign() + .key(key) + .withKeyPassword("sw0rdf1sh") + .mode(InlineSignAs.clearsigned) + .data(message).getBytes(); + + ByteArrayAndResult> result = sop.inlineVerify() + .cert(cert) + .data(inlineSigned) + .toByteArrayAndResult(); + + byte[] verified = result.getBytes(); + + List verificationList = result.getResult(); + VerificationListAssert.assertThatVerificationList(verificationList) + .hasSingleItem() + .hasMode(SignatureMode.text); + + assertArrayEquals(message, verified); + } + + @Test + public void testInlineSignAndVerifyWithBinarySignatures() throws IOException { + byte[] key = sop.generateKey() + .userId("Werner") + .withKeyPassword("sw0rdf1sh") + .generate().getBytes(); + + byte[] cert = sop.extractCert() + .key(key).getBytes(); + + byte[] message = "Yes, this is what has been deployed worldwide for years in millions of\ninstallations (decryption wise) and is meanwhile in active use.\n".getBytes(StandardCharsets.UTF_8); + + byte[] inlineSigned = sop.inlineSign() + .key(key) + .withKeyPassword("sw0rdf1sh") + .mode(InlineSignAs.binary) + .data(message).getBytes(); + + ByteArrayAndResult> result = sop.inlineVerify() + .cert(cert) + .data(inlineSigned) + .toByteArrayAndResult(); + + byte[] verified = result.getBytes(); + + List verificationList = result.getResult(); + VerificationListAssert.assertThatVerificationList(verificationList) + .hasSingleItem() + .hasMode(SignatureMode.binary); + + assertArrayEquals(message, verified); + } + + + @Test + public void testInlineSignAndVerifyWithTextSignatures() throws IOException { + byte[] key = sop.generateKey() + .userId("Mark") + .withKeyPassword("y3110w5ubm4r1n3") + .generate().getBytes(); + + byte[] cert = sop.extractCert() + .key(key).getBytes(); + + byte[] message = "Give me a plaintext that I can sign and verify, pls.".getBytes(StandardCharsets.UTF_8); + + byte[] inlineSigned = sop.inlineSign() + .key(key) + .withKeyPassword("y3110w5ubm4r1n3") + .mode(InlineSignAs.text) + .data(message).getBytes(); + + ByteArrayAndResult> result = sop.inlineVerify() + .cert(cert) + .data(inlineSigned) + .toByteArrayAndResult(); + + byte[] verified = result.getBytes(); + + List verificationList = result.getResult(); + VerificationListAssert.assertThatVerificationList(verificationList) + .hasSingleItem() + .hasMode(SignatureMode.text); + + assertArrayEquals(message, verified); + } + +} diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/ListProfilesTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/ListProfilesTest.java new file mode 100644 index 00000000..e15a04cc --- /dev/null +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/ListProfilesTest.java @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import sop.SOP; +import sop.exception.SOPGPException; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class ListProfilesTest { + + private SOP sop; + + @BeforeEach + public void prepare() { + this.sop = new SOPImpl(); + } + + @Test + public void listProfilesOfGenerateKey() { + assertFalse(sop.listProfiles().subcommand("generate-key").isEmpty()); + } + + @Test + public void listProfilesOfEncrypt() { + assertFalse(sop.listProfiles().encrypt().isEmpty()); + } + + @Test + public void listProfilesOfHelpCommandThrows() { + assertThrows(SOPGPException.UnsupportedProfile.class, () -> + sop.listProfiles().subcommand("help")); + } + +} diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/MatchMakingSecretKeyRingProtectorTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/MatchMakingSecretKeyRingProtectorTest.java new file mode 100644 index 00000000..9dc2b6c6 --- /dev/null +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/MatchMakingSecretKeyRingProtectorTest.java @@ -0,0 +1,108 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.util.Passphrase; + +public class MatchMakingSecretKeyRingProtectorTest { + + private static final String PROTECTED_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 0221 626A 3B5A 4705 7A41 7EAB 2B9F C90E 44FA 1947\n" + + "Comment: Alice\n" + + "\n" + + "lIYEY3zkcRYJKwYBBAHaRw8BAQdAzww+ctlV7imTD/LSQlVn3onybSvQa54CIUaN\n" + + "xN9FDFH+CQMCqDw0ZfofkfxgK7+uSfi7btqa6+o+zGkKfKQCvYCuU5gorD7vyOFL\n" + + "2ezeQOjb17HHaKbJqLrx+p+LS2uU2f3cwa73PFHwNcBoDLRTrUXjzrQFQWxpY2WI\n" + + "jwQTFgoAQQUCY3zkcQkQK5/JDkT6GUcWIQQCIWJqO1pHBXpBfqsrn8kORPoZRwKe\n" + + "AQKbAQUWAgMBAAQLCQgHBRUKCQgLApkBAABTCQD9HCDmb8LlO+n/5jJv7n6gAHCA\n" + + "UUNAe7xU4WcYSxLpTPIBAOxBmbZiDai0QwDOqNihpwrInu82fRi8OEpSjE/9OrEC\n" + + "nIsEY3zkcRIKKwYBBAGXVQEFAQEHQBsnJtVYXMaGB4BDcUEKB1v/lsXJ1z+favfn\n" + + "e73/crYEAwEIB/4JAwKoPDRl+h+R/GBdZY7QJt8TPaXckyOR1eZvUejD+Vw/slB1\n" + + "3KUwGI/3MG2iJYp924wP67DewZI89eYHu24wN75XxVKAGnUX5n7Dr2JIB79liHUE\n" + + "GBYKAB0FAmN85HECngECmwwFFgIDAQAECwkIBwUVCgkICwAKCRArn8kORPoZR5bF\n" + + "APsHLmhDRDV2Ra0BsfQRNI2yMXxVRFD/ZzryWFT/BPNGUAEA9cnhItp9ucqBbeWE\n" + + "PDzf1vdx5BCNYhRpOqGjGtFgMQGchgRjfORxFgkrBgEEAdpHDwEBB0AAvsniUT76\n" + + "OeLyq9e1bgdsDLiGrtroSt6wR/B94Dm5uP4JAwKoPDRl+h+R/GC8mQnynSzBJXdy\n" + + "DFDnxOieEOh7390vs3P4NwULTqV12sAQ6i5MbsIHnFMtYCCA9aOPlpofQ0Sm3m6q\n" + + "T/uyx9RE1LRiceW5iNUEGBYKAH0FAmN85HECngECmwIFFgIDAQAECwkIBwUVCgkI\n" + + "C18gBBkWCgAGBQJjfORxAAoJEGiOcMMZDPmyw7QBAJuNTLiNWgieuGOVCAmkaN6g\n" + + "L6JlYYwqFS88zzDLJJq5AQDwKt+jvKco6Mya3b1NEXogBLhWHTle9deL07NrCwp4\n" + + "AwAKCRArn8kORPoZR7r0AP0TDUKaooNfW2MWqLWHbbIhdWFIQEYIGnGSFj28y6t1\n" + + "zQD/UFtpzBP5ZlTUtZCdjNqo9SEPktbiOxTS8m4SW7xeNwE=\n" + + "=91/N\n" + + "-----END PGP PRIVATE KEY BLOCK-----\n"; + private static final String PASSWORD = "sw0rdf1sh"; + private static final String UNPROTECTED_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 9A0A F461 3E00 E1D4 4C29 601E AAF1 12F4 64BB 1E8E\n" + + "Comment: Bob\n" + + "\n" + + "lFgEY3zlDBYJKwYBBAHaRw8BAQdAvHofWedfSBvyW2+gCADX9CptwFzqVea4A2tL\n" + + "zr3wnwsAAP9ICAoMGkgdNLy3LiVP0q4+OljXcQTIAJbJ2wCpIF9Y7g05tANCb2KI\n" + + "jwQTFgoAQQUCY3zlDAkQqvES9GS7Ho4WIQSaCvRhPgDh1EwpYB6q8RL0ZLsejgKe\n" + + "AQKbAQUWAgMBAAQLCQgHBRUKCQgLApkBAAAZ8QD/fEW105B77KBt/OmA0QLTq3GG\n" + + "5PI6kITM8+2cd60VOzEA/ivzkhmtdvHzmOARBl81Y3LfeRWWm45z/dYDnffk/DcI\n" + + "nF0EY3zlDBIKKwYBBAGXVQEFAQEHQC1sYpvzEsjCoTOKEllFkWA3U51FXsHbbALq\n" + + "QfprOrYKAwEIBwAA/3H4zdk83/0A55hJxBIgh3v/+EV1RKPDCjMHjI5ULc7AEa6I\n" + + "dQQYFgoAHQUCY3zlDAKeAQKbDAUWAgMBAAQLCQgHBRUKCQgLAAoJEKrxEvRkux6O\n" + + "pEsA/imXUEpj6mKkT4ZBioT7Gn2mUR4iMGS/pt7QBscDX2/PAP9FFRzsaDII1K+i\n" + + "zW5sHEif9EjgX6ThIpg8z4/5/7yQBZxYBGN85QwWCSsGAQQB2kcPAQEHQDTAYxP0\n" + + "rH0tjpOKOxdoHKq87n4tYXd1t/A9Nzjbl36AAAD+PMBIpNmN+k3THARd9UGQtLo4\n" + + "nieLnqbuPVtMps0kQjgQxojVBBgWCgB9BQJjfOUMAp4BApsCBRYCAwEABAsJCAcF\n" + + "FQoJCAtfIAQZFgoABgUCY3zlDAAKCRB0YfQ676jh4zoMAP98SwGcoy8Vzk8QnQ0X\n" + + "gziC+4HtmTLuiDVAvrMLpPz5cwD8C40DDHEjrOJs9bgyOeTELXtjq40Wrt2Fld0G\n" + + "3JJpFAwACgkQqvES9GS7Ho7EXwD7BwICVWrg458XKpy2EXGSI3mGA47EbyyFc9X3\n" + + "lBzjnCgA/jUBlZE2LhpAyMTbjDC9eAD1iXeTALdRKBeqnZrQTL0N\n" + + "=OypZ\n" + + "-----END PGP PRIVATE KEY BLOCK-----\n"; + + @Test + public void addSamePasswordTwice() throws IOException { + PGPSecretKeyRing key = PGPainless.readKeyRing().secretKeyRing(PROTECTED_KEY); + MatchMakingSecretKeyRingProtector protector = new MatchMakingSecretKeyRingProtector(); + protector.addPassphrase(Passphrase.fromPassword(PASSWORD)); + protector.addPassphrase(Passphrase.fromPassword(PASSWORD)); + protector.addSecretKey(key); + + assertTrue(protector.hasPassphraseFor(key.getPublicKey().getKeyID())); + } + + @Test + public void addKeyTwiceAndEmptyPasswordTest() throws IOException { + PGPSecretKeyRing unprotectedKey = PGPainless.readKeyRing().secretKeyRing(UNPROTECTED_KEY); + MatchMakingSecretKeyRingProtector protector = new MatchMakingSecretKeyRingProtector(); + protector.addSecretKey(unprotectedKey); + protector.addPassphrase(Passphrase.emptyPassphrase()); + protector.addSecretKey(unprotectedKey); + assertTrue(protector.hasPassphraseFor(unprotectedKey.getPublicKey().getKeyID())); + } + + @Test + public void getEncryptorTest() throws IOException, PGPException { + PGPSecretKeyRing unprotectedKey = PGPainless.readKeyRing().secretKeyRing(UNPROTECTED_KEY); + MatchMakingSecretKeyRingProtector protector = new MatchMakingSecretKeyRingProtector(); + protector.addSecretKey(unprotectedKey); + assertTrue(protector.hasPassphraseFor(unprotectedKey.getPublicKey().getKeyID())); + assertNull(protector.getEncryptor(unprotectedKey.getPublicKey().getKeyID())); + assertNull(protector.getDecryptor(unprotectedKey.getPublicKey().getKeyID())); + + PGPSecretKeyRing protectedKey = PGPainless.readKeyRing().secretKeyRing(PROTECTED_KEY); + protector.addSecretKey(protectedKey); + protector.addPassphrase(Passphrase.fromPassword(PASSWORD)); + assertNotNull(protector.getEncryptor(protectedKey.getPublicKey().getKeyID())); + assertNotNull(protector.getDecryptor(protectedKey.getPublicKey().getKeyID())); + } +} diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/VerifyLegacySignatureTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/VerifyLegacySignatureTest.java new file mode 100644 index 00000000..23fd9840 --- /dev/null +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/VerifyLegacySignatureTest.java @@ -0,0 +1,164 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop; + +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.policy.Policy; +import sop.ByteArrayAndResult; +import sop.Verification; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +public class VerifyLegacySignatureTest { + + @Test + public void verifyLegacySignature() throws IOException { + // Key generated in 2012 using SHA1 for self sigs + String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 21FA 6E60 D6C9 B7D1 0EAC 56A2 984B 91CF D303 214C\n" + + "Comment: Legacy \n" + + "\n" + + "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\n" + + "SV9g8CiU0cX7hiBYYpktcDVU3uRCR4Fkvw==\n" + + "=n8qw\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + // Sig generated in 2012 using SHA1 + String oldSig = "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "owEB2gEl/pANAwACAZhLkc/TAyFMAcsTYgAAAAAASGVsbG8sIFdvcmxkIYkBswQA\n" + + "AQIAJwkQmEuRz9MDIUwWIQQh+m5g1sm30Q6sVqKYS5HP0wMhTAWCUNRWwAAALxEL\n" + + "/2uhYsTLM8nUnYm2GJB6pkapX1kbQrqfAhK46IjxcPpRdl6CW4cFrG6iFegx4YLE\n" + + "fu44VKG+XGy/RTZXIEJubi9zVyOGGJM9Bwwdcp/eekO16/kJ7BsbkaO+5AG/fNeg\n" + + "bL5C8D2m6jV1seAt/+tRyM9jLkRi9odq8BsGA6ZcthAxh3MUoo1yw3QwwEcFFHg/\n" + + "gBw4ZtL8KIQN1PKDz3sSV4GXPQAiz+/uADZ2lL6mbDEK/gXAK1KevIO3U8ZU9B6l\n" + + "cOF9fJww31SCqFGDq50Lzwz7eySJB1TZ0IoehGDXoQ8JF88uTVfACkBATE0Zx7zg\n" + + "TAYIgPSjWY4TEDZ9YjdxJ0hKTMncxVfZPB+J/mYCpVADYSEhLbUJ1ntjc0s35xJD\n" + + "udLSwUWuboedVdEcaqnfgHoaaV+nKk+6F9y8NO56RK3Bfx5FmKmNZHbhfXO/qRt9\n" + + "H43UktMUD6xWxxJv7mutThOp2aizBeboa5YSJ1mxtkPW0/lyK1jr438ETHUnCeu6\n" + + "Vw==\n" + + "=TtKx\n" + + "-----END PGP MESSAGE-----"; + + SOPImpl sop = new SOPImpl(); + byte[] cert = sop.extractCert().key(KEY.getBytes(StandardCharsets.UTF_8)) + .getBytes(); + ByteArrayAndResult> result = sop.inlineVerify() + .cert(cert) + .data(oldSig.getBytes(StandardCharsets.UTF_8)) + .toByteArrayAndResult(); + + assertFalse(result.getResult().isEmpty()); + + // Adjust data signature hash policy to accept new SHA-1 sigs + PGPainless.getPolicy().setDataSignatureHashAlgorithmPolicy( + Policy.HashAlgorithmPolicy.static2022RevocationSignatureHashAlgorithmPolicy()); + + // Sig generated in 2024 using SHA1 + String newSig = "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "owEB2gEl/pANAwACAZhLkc/TAyFMAcsTYgAAAAAASGVsbG8sIFdvcmxkIYkBswQA\n" + + "AQIAJwWCZw5i2AkQmEuRz9MDIUwWIQQh+m5g1sm30Q6sVqKYS5HP0wMhTAAAhVML\n" + + "+QGH+O2fEJoAY8ZxKz/mosg4it9IeSzMhBvDgZJE8Jc+VGk7EuXL0M8pfHL+Jgmv\n" + + "FMzF3chzzLS7QA4K6hbxO31/M8TNSU12geuzQiBV7Kb1hjpvIObBgEqYsX50ZV8r\n" + + "5DHcr7huABUOH6tCKmCA2OxOvr1QV8X39h856bz3WqqP9HW8kZ6H1Z6d7XWlRMtW\n" + + "mAnSevvOJbb0Z3D97obYqytSLzi2Jyv+w2R9kYzMQff2Rl6Cv4F7zsRrF9JRC0m6\n" + + "X/s+VSNuT2yG0/4F5y8vrxvNkfd8YfM8DM6irJV4yJyVuwIoZnM913XCA4F7Uo4t\n" + + "Z8ER17SY4WOYvdja/7qPcOUjX5n1dDU0W7q2muqnZXREw2JXTULiDl0MET3K4kFu\n" + + "a6FyyMGGQwFpAnZ4gDZKzw06abd95AgHx4QlkD89J7MnUBBV+AGHNAQlCPPEVPQq\n" + + "dWTInYndt4GKCUxVkJeHD6ZPLdxEEvICmEap4FQzhqM8U7weoEsSinoVoc4JmSY9\n" + + "dQ==\n" + + "=XrzP\n" + + "-----END PGP MESSAGE-----"; + result = sop.inlineVerify() + .cert(cert) + .data(newSig.getBytes(StandardCharsets.UTF_8)) + .toByteArrayAndResult(); + + assertFalse(result.getResult().isEmpty()); + } +} diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/VersionTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/VersionTest.java index 712df550..825b5e86 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/VersionTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/VersionTest.java @@ -5,19 +5,78 @@ package org.pgpainless.sop; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import sop.SOP; public class VersionTest { + private static SOP sop; + + @BeforeAll + public static void setup() { + sop = new SOPImpl(); + } + @Test public void testGetVersion() { - assertNotNull(new SOPImpl().version().getVersion()); + String version = sop.version().getVersion(); + assertNotNull(version); + assertFalse(version.isEmpty()); } @Test public void assertNameEqualsPGPainless() { - assertEquals("PGPainless-SOP", new SOPImpl().version().getName()); + assertEquals("PGPainless-SOP", sop.version().getName()); + } + + @Test + public void testGetBackendVersion() { + String backendVersion = sop.version().getBackendVersion(); + assertNotNull(backendVersion); + assertFalse(backendVersion.isEmpty()); + } + + @Test + public void testGetExtendedVersion() { + String extendedVersion = sop.version().getExtendedVersion(); + assertNotNull(extendedVersion); + assertFalse(extendedVersion.isEmpty()); + + String firstLine = extendedVersion.split("\n")[0]; + assertEquals(sop.version().getName() + " " + sop.version().getVersion(), firstLine); + } + + @Test + public void testGetSopSpecVersion() { + boolean incomplete = sop.version().isSopSpecImplementationIncomplete(); + int revisionNumber = sop.version().getSopSpecRevisionNumber(); + + String revisionString = sop.version().getSopSpecRevisionName(); + assertEquals("draft-dkg-openpgp-stateless-cli-" + String.format("%02d", revisionNumber), revisionString); + + String incompletenessRemarks = sop.version().getSopSpecImplementationRemarks(); + + String fullSopSpecVersion = sop.version().getSopSpecVersion(); + if (incomplete) { + assertTrue(fullSopSpecVersion.startsWith("~" + revisionString)); + } else { + assertTrue(fullSopSpecVersion.startsWith(revisionString)); + } + + if (incompletenessRemarks != null) { + assertTrue(fullSopSpecVersion.endsWith(incompletenessRemarks)); + } + } + + @Test + public void testGetSopVVersion() { + String sopVVersion = sop.version().getSopVVersion(); + assertNotNull(sopVVersion); + assertTrue(sopVVersion.matches("\\d+\\.\\d+(\\.\\d+)*")); // X.Y or X.Y.Z... etc. } } diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/package-info.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/package-info.java new file mode 100644 index 00000000..a1994b9d --- /dev/null +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/package-info.java @@ -0,0 +1,9 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +/** + * Tests for the {@link sop.SOP} API, tailored to the behavior of PGPainless' implementation specifically. + * Generalized tests can be found in {@link sop.testsuite.pgpainless}. + */ +package org.pgpainless.sop; diff --git a/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/PGPainlessSopInstanceFactory.java b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/PGPainlessSopInstanceFactory.java new file mode 100644 index 00000000..a9aac0e9 --- /dev/null +++ b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/PGPainlessSopInstanceFactory.java @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.pgpainless; + +import java.util.Collections; +import java.util.Map; + +import org.pgpainless.sop.SOPImpl; +import sop.SOP; +import sop.testsuite.SOPInstanceFactory; + +public class PGPainlessSopInstanceFactory extends SOPInstanceFactory { + + @Override + public Map provideSOPInstances() { + return Collections.singletonMap("PGPainless-SOP", new SOPImpl()); + } +} diff --git a/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessArmorDearmorTest.java b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessArmorDearmorTest.java new file mode 100644 index 00000000..9161706d --- /dev/null +++ b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessArmorDearmorTest.java @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.pgpainless.operation; + +import sop.testsuite.operation.ArmorDearmorTest; + +public class PGPainlessArmorDearmorTest extends ArmorDearmorTest { + +} diff --git a/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessChangeKeyPasswordTest.java b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessChangeKeyPasswordTest.java new file mode 100644 index 00000000..baf595d3 --- /dev/null +++ b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessChangeKeyPasswordTest.java @@ -0,0 +1,89 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.pgpainless.operation; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.KeyFlag; +import org.pgpainless.key.generation.KeySpec; +import org.pgpainless.key.generation.type.KeyType; +import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve; +import org.pgpainless.key.generation.type.xdh_legacy.XDHLegacySpec; +import org.pgpainless.util.Passphrase; +import sop.SOP; +import sop.testsuite.TestData; +import sop.testsuite.operation.ChangeKeyPasswordTest; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.util.Iterator; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +public class PGPainlessChangeKeyPasswordTest extends ChangeKeyPasswordTest { + + @ParameterizedTest + @MethodSource("provideInstances") + public void changePasswordOfKeyWithSeparateSubkeyPasswords(SOP sop) throws IOException, PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER)) + .addSubkey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.SIGN_DATA)) + .addSubkey(KeySpec.getBuilder(KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)) + .build(); + Iterator keys = secretKeys.getPublicKeys(); + long primaryKeyId = keys.next().getKeyID(); + long signingKeyId = keys.next().getKeyID(); + long encryptKeyId = keys.next().getKeyID(); + + String p1 = "sw0rdf1sh"; + String p2 = "0r4ng3"; + String p3 = "dr4g0n"; + + secretKeys = PGPainless.modifyKeyRing(secretKeys) + .changeSubKeyPassphraseFromOldPassphrase(primaryKeyId, Passphrase.emptyPassphrase()) + .withSecureDefaultSettings() + .toNewPassphrase(Passphrase.fromPassword(p1)) + .changeSubKeyPassphraseFromOldPassphrase(signingKeyId, Passphrase.emptyPassphrase()) + .withSecureDefaultSettings() + .toNewPassphrase(Passphrase.fromPassword(p2)) + .changeSubKeyPassphraseFromOldPassphrase(encryptKeyId, Passphrase.emptyPassphrase()) + .withSecureDefaultSettings() + .toNewPassphrase(Passphrase.fromPassword(p3)) + .done(); + + String p4 = "m0nk3y"; + + byte[] keyWithNewPassword = sop.changeKeyPassword() + .oldKeyPassphrase(p1) + .oldKeyPassphrase(p2) + .oldKeyPassphrase(p3) + .newKeyPassphrase(p4) + .keys(secretKeys.getEncoded()) + .getBytes(); + byte[] cert = sop.extractCert().key(keyWithNewPassword).getBytes(); + + byte[] signedAndEncrypted = sop.encrypt() + .signWith(keyWithNewPassword) + .withKeyPassword(p4) + .withCert(cert) + .plaintext(TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8)) + .toByteArrayAndResult() + .getBytes(); + byte[] plaintext = sop.decrypt() + .verifyWithCert(cert) + .withKey(keyWithNewPassword) + .withKeyPassword(p4) + .ciphertext(signedAndEncrypted) + .toByteArrayAndResult().getBytes(); + + assertArrayEquals(TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8), plaintext); + } +} diff --git a/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessDecryptWIthSessionKeyTest.java b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessDecryptWIthSessionKeyTest.java new file mode 100644 index 00000000..2825db5f --- /dev/null +++ b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessDecryptWIthSessionKeyTest.java @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.pgpainless.operation; + +import sop.testsuite.operation.DecryptWithSessionKeyTest; + +public class PGPainlessDecryptWIthSessionKeyTest extends DecryptWithSessionKeyTest { + +} diff --git a/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessDetachedSignDetachedVerifyTest.java b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessDetachedSignDetachedVerifyTest.java new file mode 100644 index 00000000..dff9e86f --- /dev/null +++ b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessDetachedSignDetachedVerifyTest.java @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.pgpainless.operation; + +import org.junit.jupiter.api.Disabled; +import sop.SOP; +import sop.testsuite.operation.DetachedSignDetachedVerifyTest; + +public class PGPainlessDetachedSignDetachedVerifyTest extends DetachedSignDetachedVerifyTest { + + @Override + @Disabled("Since we allow for dynamic cert loading, we can ignore this test") + public void verifyMissingCertCausesMissingArg(SOP sop) { + super.verifyMissingCertCausesMissingArg(sop); + } +} diff --git a/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessEncryptDecryptTest.java b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessEncryptDecryptTest.java new file mode 100644 index 00000000..b6264d3a --- /dev/null +++ b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessEncryptDecryptTest.java @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.pgpainless.operation; + +import sop.testsuite.operation.EncryptDecryptTest; + +public class PGPainlessEncryptDecryptTest extends EncryptDecryptTest { + +} diff --git a/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessExtractCertTest.java b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessExtractCertTest.java new file mode 100644 index 00000000..ee4e9684 --- /dev/null +++ b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessExtractCertTest.java @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.pgpainless.operation; + +import java.io.IOException; + +import org.junit.jupiter.api.Disabled; +import sop.SOP; +import sop.testsuite.operation.ExtractCertTest; + +public class PGPainlessExtractCertTest extends ExtractCertTest { + + @Disabled("BC uses old CTBs causing mismatching byte arrays :/") + @Override + public void extractAliceCertFromAliceKeyTest(SOP sop) throws IOException { + super.extractAliceCertFromAliceKeyTest(sop); + } + + @Disabled("BC uses old CTBs causing mismatching byte arrays :/") + @Override + public void extractBobsCertFromBobsKeyTest(SOP sop) throws IOException { + super.extractBobsCertFromBobsKeyTest(sop); + } + + @Disabled("BC uses old CTBs causing mismatching byte arrays :/") + @Override + public void extractCarolsCertFromCarolsKeyTest(SOP sop) throws IOException { + super.extractCarolsCertFromCarolsKeyTest(sop); + } +} diff --git a/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessGenerateKeyTest.java b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessGenerateKeyTest.java new file mode 100644 index 00000000..fe78bed0 --- /dev/null +++ b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessGenerateKeyTest.java @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.pgpainless.operation; + +import sop.testsuite.operation.GenerateKeyTest; + +public class PGPainlessGenerateKeyTest extends GenerateKeyTest { + +} diff --git a/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessInlineSignInlineDetachDetachedVerifyTest.java b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessInlineSignInlineDetachDetachedVerifyTest.java new file mode 100644 index 00000000..20fdc262 --- /dev/null +++ b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessInlineSignInlineDetachDetachedVerifyTest.java @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.pgpainless.operation; + +import sop.testsuite.operation.InlineSignInlineDetachDetachedVerifyTest; + +public class PGPainlessInlineSignInlineDetachDetachedVerifyTest + extends InlineSignInlineDetachDetachedVerifyTest { + +} diff --git a/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessInlineSignInlineVerifyTest.java b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessInlineSignInlineVerifyTest.java new file mode 100644 index 00000000..16166eb1 --- /dev/null +++ b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessInlineSignInlineVerifyTest.java @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.pgpainless.operation; + +import sop.testsuite.operation.InlineSignInlineVerifyTest; + +public class PGPainlessInlineSignInlineVerifyTest extends InlineSignInlineVerifyTest { + +} diff --git a/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessRevokeKeyTest.java b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessRevokeKeyTest.java new file mode 100644 index 00000000..b7590b7a --- /dev/null +++ b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessRevokeKeyTest.java @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.pgpainless.operation; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; + +import org.bouncycastle.openpgp.PGPKeyRing; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.pgpainless.PGPainless; +import org.pgpainless.key.info.KeyRingInfo; +import sop.SOP; +import sop.testsuite.operation.RevokeKeyTest; + +public class PGPainlessRevokeKeyTest extends RevokeKeyTest { + + @ParameterizedTest + @MethodSource("provideInstances") // from sop-java's RevokeKeyTest class + @Override + public void revokeUnprotectedKey(SOP sop) throws IOException { + super.revokeUnprotectedKey(sop); + + byte[] key = sop.generateKey().generate().getBytes(); + byte[] revokedKey = sop.revokeKey().keys(key).getBytes(); + + PGPKeyRing certificate = PGPainless.readKeyRing().keyRing(revokedKey); + assertFalse(certificate instanceof PGPSecretKeyRing); + assertTrue(certificate instanceof PGPPublicKeyRing); + + KeyRingInfo info = PGPainless.inspectKeyRing(certificate); + assertTrue(info.getRevocationState().isHardRevocation()); + } +} diff --git a/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessVersionTest.java b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessVersionTest.java new file mode 100644 index 00000000..7a8f7db4 --- /dev/null +++ b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessVersionTest.java @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.pgpainless.operation; + +import sop.testsuite.operation.VersionTest; + +public class PGPainlessVersionTest extends VersionTest { + +} diff --git a/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/package-info.java b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/package-info.java new file mode 100644 index 00000000..5da4307d --- /dev/null +++ b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/package-info.java @@ -0,0 +1,9 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +/** + * Generalized tests for the {@link sop.SOP} API. + * For tests tailored specifically to PGPainless' behavior, see {@link org.pgpainless.sop}. + */ +package sop.testsuite.pgpainless; diff --git a/settings.gradle b/settings.gradle index 7ebc7ad4..aea19392 100644 --- a/settings.gradle +++ b/settings.gradle @@ -5,8 +5,6 @@ rootProject.name = 'PGPainless' include 'pgpainless-core', - 'sop-java', 'pgpainless-sop', - 'sop-java-picocli', 'pgpainless-cli' diff --git a/sop-java-picocli/README.md b/sop-java-picocli/README.md deleted file mode 100644 index f76c9295..00000000 --- a/sop-java-picocli/README.md +++ /dev/null @@ -1,34 +0,0 @@ - -# SOP-Java-Picocli - -Implementation of the [Stateless OpenPGP Command Line Interface](https://tools.ietf.org/html/draft-dkg-openpgp-stateless-cli-01) specification. -This terminal application allows generation of OpenPGP keys, extraction of public key certificates, -armoring and de-armoring of data, as well as - of course - encryption/decryption of messages and creation/verification of signatures. - -## Install a SOP backend - -This module comes without a SOP backend, so in order to function you need to extend it with an implementation of the interfaces defined in `sop-java`. -An implementation using PGPainless can be found in the module `pgpainless-sop`, but it is of course possible to provide your -own implementation. - -Just install your SOP backend by calling -```java -// static method call prior to execution of the main method -SopCLI.setSopInstance(yourSopImpl); -``` - -## Usage - -To get an overview of available commands of the application, execute -```shell -java -jar sop-java-picocli-XXX.jar help -``` - -If you just want to get started encrypting messages, see the module `pgpainless-cli` which initializes -`sop-java-picocli` with `pgpainless-sop`, so you can get started right away without the need to manually wire stuff up. - -Enjoy! \ No newline at end of file diff --git a/sop-java-picocli/build.gradle b/sop-java-picocli/build.gradle deleted file mode 100644 index 69cd1c52..00000000 --- a/sop-java-picocli/build.gradle +++ /dev/null @@ -1,38 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -plugins { - id 'application' -} - -dependencies { - testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion" - testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion" - // https://todd.ginsberg.com/post/testing-system-exit/ - testImplementation 'com.ginsberg:junit5-system-exit:1.1.1' - testImplementation "org.mockito:mockito-core:3.11.2" - - implementation(project(":sop-java")) - implementation "info.picocli:picocli:$picocliVersion" - - // https://mvnrepository.com/artifact/com.google.code.findbugs/jsr305 - implementation group: 'com.google.code.findbugs', name: 'jsr305', version: '3.0.2' -} - -mainClassName = 'sop.cli.picocli.SopCLI' - -jar { - manifest { - attributes 'Main-Class': "$mainClassName" - } - - from { - configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } - } { - exclude "META-INF/*.SF" - exclude "META-INF/*.DSA" - exclude "META-INF/*.RSA" - } -} - diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/DateParser.java b/sop-java-picocli/src/main/java/sop/cli/picocli/DateParser.java deleted file mode 100644 index d2e2188e..00000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/DateParser.java +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli; - -import java.util.Date; - -import sop.util.UTCUtil; - -public class DateParser { - - public static final Date BEGINNING_OF_TIME = new Date(0); - public static final Date END_OF_TIME = new Date(8640000000000000L); - - public static Date parseNotAfter(String notAfter) { - Date date = notAfter.equals("now") ? new Date() : notAfter.equals("-") ? END_OF_TIME : UTCUtil.parseUTCDate(notAfter); - if (date == null) { - Print.errln("Invalid date string supplied as value of --not-after."); - System.exit(1); - } - return date; - } - - public static Date parseNotBefore(String notBefore) { - Date date = notBefore.equals("now") ? new Date() : notBefore.equals("-") ? BEGINNING_OF_TIME : UTCUtil.parseUTCDate(notBefore); - if (date == null) { - Print.errln("Invalid date string supplied as value of --not-before."); - System.exit(1); - } - return date; - } -} diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/FileUtil.java b/sop-java-picocli/src/main/java/sop/cli/picocli/FileUtil.java deleted file mode 100644 index cd92e6db..00000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/FileUtil.java +++ /dev/null @@ -1,98 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; - -import sop.exception.SOPGPException; - -public class FileUtil { - - private static final String ERROR_AMBIGUOUS = "File name '%s' is ambiguous. File with the same name exists on the filesystem."; - private static final String ERROR_ENV_FOUND = "Environment variable '%s' not set."; - private static final String ERROR_OUTPUT_EXISTS = "Output file '%s' already exists."; - private static final String ERROR_INPUT_NOT_EXIST = "File '%s' does not exist."; - private static final String ERROR_CANNOT_CREATE_FILE = "Output file '%s' cannot be created: %s"; - - public static final String PRFX_ENV = "@ENV:"; - public static final String PRFX_FD = "@FD:"; - - private static EnvironmentVariableResolver envResolver = System::getenv; - - public static void setEnvironmentVariableResolver(EnvironmentVariableResolver envResolver) { - if (envResolver == null) { - throw new NullPointerException("Variable envResolver cannot be null."); - } - FileUtil.envResolver = envResolver; - } - - public interface EnvironmentVariableResolver { - /** - * Resolve the value of the given environment variable. - * Return null if the variable is not present. - * - * @param name name of the variable - * @return variable value or null - */ - String resolveEnvironmentVariable(String name); - } - - public static File getFile(String fileName) { - if (fileName == null) { - throw new NullPointerException("File name cannot be null."); - } - - if (fileName.startsWith(PRFX_ENV)) { - - if (new File(fileName).exists()) { - throw new SOPGPException.AmbiguousInput(String.format(ERROR_AMBIGUOUS, fileName)); - } - - String envName = fileName.substring(PRFX_ENV.length()); - String envValue = envResolver.resolveEnvironmentVariable(envName); - if (envValue == null) { - throw new IllegalArgumentException(String.format(ERROR_ENV_FOUND, envName)); - } - return new File(envValue); - } else if (fileName.startsWith(PRFX_FD)) { - - if (new File(fileName).exists()) { - throw new SOPGPException.AmbiguousInput(String.format(ERROR_AMBIGUOUS, fileName)); - } - - throw new IllegalArgumentException("File descriptors not supported."); - } - - return new File(fileName); - } - - public static FileInputStream getFileInputStream(String fileName) { - File file = getFile(fileName); - try { - FileInputStream inputStream = new FileInputStream(file); - return inputStream; - } catch (FileNotFoundException e) { - throw new SOPGPException.MissingInput(String.format(ERROR_INPUT_NOT_EXIST, fileName), e); - } - } - - public static File createNewFileOrThrow(File file) throws IOException { - if (file == null) { - throw new NullPointerException("File cannot be null."); - } - - try { - if (!file.createNewFile()) { - throw new SOPGPException.OutputExists(String.format(ERROR_OUTPUT_EXISTS, file.getAbsolutePath())); - } - } catch (IOException e) { - throw new IOException(String.format(ERROR_CANNOT_CREATE_FILE, file.getAbsolutePath(), e.getMessage())); - } - return file; - } -} diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/Print.java b/sop-java-picocli/src/main/java/sop/cli/picocli/Print.java deleted file mode 100644 index d6474e1d..00000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/Print.java +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli; - -public class Print { - - public static void errln(String string) { - // CHECKSTYLE:OFF - System.err.println(string); - // CHECKSTYLE:ON - } - - public static void trace(Throwable e) { - // CHECKSTYLE:OFF - e.printStackTrace(); - // CHECKSTYLE:ON - } - - public static void outln(String string) { - // CHECKSTYLE:OFF - System.out.println(string); - // CHECKSTYLE:ON - } -} diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/SOPExceptionExitCodeMapper.java b/sop-java-picocli/src/main/java/sop/cli/picocli/SOPExceptionExitCodeMapper.java deleted file mode 100644 index 8b38af32..00000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/SOPExceptionExitCodeMapper.java +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli; - -import picocli.CommandLine; -import sop.exception.SOPGPException; - -public class SOPExceptionExitCodeMapper implements CommandLine.IExitCodeExceptionMapper { - - @Override - public int getExitCode(Throwable exception) { - if (exception instanceof SOPGPException) { - return ((SOPGPException) exception).getExitCode(); - } - if (exception instanceof CommandLine.UnmatchedArgumentException) { - CommandLine.UnmatchedArgumentException ex = (CommandLine.UnmatchedArgumentException) exception; - // Unmatched option of subcommand (eg. `generate-key -k`) - if (ex.isUnknownOption()) { - return SOPGPException.UnsupportedOption.EXIT_CODE; - } - // Unmatched subcommand - return SOPGPException.UnsupportedSubcommand.EXIT_CODE; - } - // Invalid option (eg. `--label Invalid`) - if (exception instanceof CommandLine.ParameterException) { - return SOPGPException.UnsupportedOption.EXIT_CODE; - } - - // Others, like IOException etc. - return 1; - } -} diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/SOPExecutionExceptionHandler.java b/sop-java-picocli/src/main/java/sop/cli/picocli/SOPExecutionExceptionHandler.java deleted file mode 100644 index dc2a047b..00000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/SOPExecutionExceptionHandler.java +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli; - -import picocli.CommandLine; - -public class SOPExecutionExceptionHandler implements CommandLine.IExecutionExceptionHandler { - - @Override - public int handleExecutionException(Exception ex, CommandLine commandLine, CommandLine.ParseResult parseResult) throws Exception { - int exitCode = commandLine.getExitCodeExceptionMapper() != null ? - commandLine.getExitCodeExceptionMapper().getExitCode(ex) : - commandLine.getCommandSpec().exitCodeOnExecutionException(); - CommandLine.Help.ColorScheme colorScheme = commandLine.getColorScheme(); - // CHECKSTYLE:OFF - if (ex.getMessage() != null) { - commandLine.getErr().println(colorScheme.errorText(ex.getMessage())); - } - ex.printStackTrace(commandLine.getErr()); - // CHECKSTYLE:ON - - return exitCode; - } -} diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/SopCLI.java b/sop-java-picocli/src/main/java/sop/cli/picocli/SopCLI.java deleted file mode 100644 index bc0ae3dd..00000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/SopCLI.java +++ /dev/null @@ -1,68 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli; - -import picocli.CommandLine; -import sop.SOP; -import sop.cli.picocli.commands.ArmorCmd; -import sop.cli.picocli.commands.DearmorCmd; -import sop.cli.picocli.commands.DecryptCmd; -import sop.cli.picocli.commands.DetachInbandSignatureAndMessageCmd; -import sop.cli.picocli.commands.EncryptCmd; -import sop.cli.picocli.commands.ExtractCertCmd; -import sop.cli.picocli.commands.GenerateKeyCmd; -import sop.cli.picocli.commands.SignCmd; -import sop.cli.picocli.commands.VerifyCmd; -import sop.cli.picocli.commands.VersionCmd; - -@CommandLine.Command( - exitCodeOnInvalidInput = 69, - subcommands = { - CommandLine.HelpCommand.class, - ArmorCmd.class, - DearmorCmd.class, - DecryptCmd.class, - DetachInbandSignatureAndMessageCmd.class, - EncryptCmd.class, - ExtractCertCmd.class, - GenerateKeyCmd.class, - SignCmd.class, - VerifyCmd.class, - VersionCmd.class - } -) -public class SopCLI { - // Singleton - static SOP SOP_INSTANCE; - - public static String EXECUTABLE_NAME = "sop"; - - public static void main(String[] args) { - int exitCode = execute(args); - if (exitCode != 0) { - System.exit(exitCode); - } - } - - public static int execute(String[] args) { - return new CommandLine(SopCLI.class) - .setCommandName(EXECUTABLE_NAME) - .setExecutionExceptionHandler(new SOPExecutionExceptionHandler()) - .setExitCodeExceptionMapper(new SOPExceptionExitCodeMapper()) - .setCaseInsensitiveEnumValuesAllowed(true) - .execute(args); - } - - public static SOP getSop() { - if (SOP_INSTANCE == null) { - throw new IllegalStateException("No SOP backend set."); - } - return SOP_INSTANCE; - } - - public static void setSopInstance(SOP instance) { - SOP_INSTANCE = instance; - } -} diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ArmorCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ArmorCmd.java deleted file mode 100644 index 139cfcdb..00000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ArmorCmd.java +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import java.io.IOException; - -import picocli.CommandLine; -import sop.Ready; -import sop.cli.picocli.Print; -import sop.cli.picocli.SopCLI; -import sop.enums.ArmorLabel; -import sop.exception.SOPGPException; -import sop.operation.Armor; - -@CommandLine.Command(name = "armor", - description = "Add ASCII Armor to standard input", - exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) -public class ArmorCmd implements Runnable { - - @CommandLine.Option(names = {"--label"}, description = "Label to be used in the header and tail of the armoring.", paramLabel = "{auto|sig|key|cert|message}") - ArmorLabel label; - - @Override - public void run() { - Armor armor = SopCLI.getSop().armor(); - if (label != null) { - try { - armor.label(label); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - Print.errln("Armor labels not supported."); - System.exit(unsupportedOption.getExitCode()); - } - } - - try { - Ready ready = armor.data(System.in); - ready.writeTo(System.out); - } catch (SOPGPException.BadData badData) { - Print.errln("Bad data."); - Print.trace(badData); - System.exit(badData.getExitCode()); - } catch (IOException e) { - Print.errln("IO Error."); - Print.trace(e); - System.exit(1); - } - } -} diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DearmorCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DearmorCmd.java deleted file mode 100644 index f3c62908..00000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DearmorCmd.java +++ /dev/null @@ -1,36 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import java.io.IOException; - -import picocli.CommandLine; -import sop.cli.picocli.Print; -import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; - -@CommandLine.Command(name = "dearmor", - description = "Remove ASCII Armor from standard input", - exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) -public class DearmorCmd implements Runnable { - - @Override - public void run() { - try { - SopCLI.getSop() - .dearmor() - .data(System.in) - .writeTo(System.out); - } catch (SOPGPException.BadData e) { - Print.errln("Bad data."); - Print.trace(e); - System.exit(e.getExitCode()); - } catch (IOException e) { - Print.errln("IO Error."); - Print.trace(e); - System.exit(1); - } - } -} diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DecryptCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DecryptCmd.java deleted file mode 100644 index 58f49db9..00000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DecryptCmd.java +++ /dev/null @@ -1,240 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.regex.Pattern; - -import picocli.CommandLine; -import sop.DecryptionResult; -import sop.ReadyWithResult; -import sop.SessionKey; -import sop.Verification; -import sop.cli.picocli.DateParser; -import sop.cli.picocli.FileUtil; -import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; -import sop.operation.Decrypt; -import sop.util.HexUtil; - -@CommandLine.Command(name = "decrypt", - description = "Decrypt a message from standard input", - exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) -public class DecryptCmd implements Runnable { - - private static final String SESSION_KEY_OUT = "--session-key-out"; - private static final String VERIFY_OUT = "--verify-out"; - - private static final String ERROR_UNSUPPORTED_OPTION = "Option '%s' is not supported."; - private static final String ERROR_FILE_NOT_EXIST = "File '%s' does not exist."; - private static final String ERROR_OUTPUT_OF_OPTION_EXISTS = "Target %s of option %s already exists."; - - @CommandLine.Option( - names = {SESSION_KEY_OUT}, - description = "Can be used to learn the session key on successful decryption", - paramLabel = "SESSIONKEY") - File sessionKeyOut; - - @CommandLine.Option( - names = {"--with-session-key"}, - description = "Enables decryption of the \"CIPHERTEXT\" using the session key directly against the \"SEIPD\" packet", - paramLabel = "SESSIONKEY") - List withSessionKey = new ArrayList<>(); - - @CommandLine.Option( - names = {"--with-password"}, - description = "Enables decryption based on any \"SKESK\" packets in the \"CIPHERTEXT\"", - paramLabel = "PASSWORD") - List withPassword = new ArrayList<>(); - - @CommandLine.Option(names = {VERIFY_OUT}, - description = "Produces signature verification status to the designated file", - paramLabel = "VERIFICATIONS") - File verifyOut; - - @CommandLine.Option(names = {"--verify-with"}, - description = "Certificates whose signatures would be acceptable for signatures over this message", - paramLabel = "CERT") - List certs = new ArrayList<>(); - - @CommandLine.Option(names = {"--not-before"}, - description = "ISO-8601 formatted UTC date (eg. '2020-11-23T16:35Z)\n" + - "Reject signatures with a creation date not in range.\n" + - "Defaults to beginning of time (\"-\").", - paramLabel = "DATE") - String notBefore = "-"; - - @CommandLine.Option(names = {"--not-after"}, - description = "ISO-8601 formatted UTC date (eg. '2020-11-23T16:35Z)\n" + - "Reject signatures with a creation date not in range.\n" + - "Defaults to current system time (\"now\").\n" + - "Accepts special value \"-\" for end of time.", - paramLabel = "DATE") - String notAfter = "now"; - - @CommandLine.Parameters(index = "0..*", - description = "Secret keys to attempt decryption with", - paramLabel = "KEY") - List keys = new ArrayList<>(); - - @Override - public void run() { - throwIfOutputExists(verifyOut, VERIFY_OUT); - throwIfOutputExists(sessionKeyOut, SESSION_KEY_OUT); - - Decrypt decrypt = SopCLI.getSop().decrypt(); - if (decrypt == null) { - throw new SOPGPException.UnsupportedSubcommand("Subcommand 'decrypt' not implemented."); - } - - setNotAfter(notAfter, decrypt); - setNotBefore(notBefore, decrypt); - setWithPasswords(withPassword, decrypt); - setWithSessionKeys(withSessionKey, decrypt); - setVerifyWith(certs, decrypt); - setDecryptWith(keys, decrypt); - - if (verifyOut != null && certs.isEmpty()) { - String errorMessage = "Option %s is requested, but no option %s was provided."; - throw new SOPGPException.IncompleteVerification(String.format(errorMessage, VERIFY_OUT, "--verify-with")); - } - - try { - ReadyWithResult ready = decrypt.ciphertext(System.in); - DecryptionResult result = ready.writeTo(System.out); - writeSessionKeyOut(result); - writeVerifyOut(result); - } catch (SOPGPException.BadData badData) { - throw new SOPGPException.BadData("No valid OpenPGP message found on Standard Input.", badData); - } catch (IOException ioException) { - throw new RuntimeException(ioException); - } - } - - private void throwIfOutputExists(File outputFile, String optionName) { - if (outputFile == null) { - return; - } - - if (outputFile.exists()) { - throw new SOPGPException.OutputExists(String.format(ERROR_OUTPUT_OF_OPTION_EXISTS, outputFile.getAbsolutePath(), optionName)); - } - } - - private void writeVerifyOut(DecryptionResult result) throws IOException { - if (verifyOut != null) { - FileUtil.createNewFileOrThrow(verifyOut); - try (FileOutputStream outputStream = new FileOutputStream(verifyOut)) { - PrintWriter writer = new PrintWriter(outputStream); - for (Verification verification : result.getVerifications()) { - // CHECKSTYLE:OFF - writer.println(verification.toString()); - // CHECKSTYLE:ON - } - writer.flush(); - } - } - } - - private void writeSessionKeyOut(DecryptionResult result) throws IOException { - if (sessionKeyOut != null) { - FileUtil.createNewFileOrThrow(sessionKeyOut); - - try (FileOutputStream outputStream = new FileOutputStream(sessionKeyOut)) { - if (!result.getSessionKey().isPresent()) { - throw new SOPGPException.UnsupportedOption("Session key not extracted. Possibly the feature --session-key-out is not supported."); - } else { - SessionKey sessionKey = result.getSessionKey().get(); - outputStream.write(sessionKey.getAlgorithm()); - outputStream.write(sessionKey.getKey()); - } - } - } - } - - private void setDecryptWith(List keys, Decrypt decrypt) { - for (File key : keys) { - try (FileInputStream keyIn = new FileInputStream(key)) { - decrypt.withKey(keyIn); - } catch (SOPGPException.KeyIsProtected keyIsProtected) { - throw new SOPGPException.KeyIsProtected("Key in file " + key.getAbsolutePath() + " is password protected.", keyIsProtected); - } catch (SOPGPException.BadData badData) { - throw new SOPGPException.BadData("File " + key.getAbsolutePath() + " does not contain a private key.", badData); - } catch (FileNotFoundException e) { - throw new SOPGPException.MissingInput(String.format(ERROR_FILE_NOT_EXIST, key.getAbsolutePath()), e); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - } - - private void setVerifyWith(List certs, Decrypt decrypt) { - for (File cert : certs) { - try (FileInputStream certIn = new FileInputStream(cert)) { - decrypt.verifyWithCert(certIn); - } catch (FileNotFoundException e) { - throw new SOPGPException.MissingInput(String.format(ERROR_FILE_NOT_EXIST, cert.getAbsolutePath()), e); - } catch (SOPGPException.BadData badData) { - throw new SOPGPException.BadData("File " + cert.getAbsolutePath() + " does not contain a valid certificate.", badData); - } catch (IOException ioException) { - throw new RuntimeException(ioException); - } - } - } - - private void setWithSessionKeys(List withSessionKey, Decrypt decrypt) { - Pattern sessionKeyPattern = Pattern.compile("^\\d+:[0-9A-F]+$"); - for (String sessionKey : withSessionKey) { - if (!sessionKeyPattern.matcher(sessionKey).matches()) { - throw new IllegalArgumentException("Session keys are expected in the format 'ALGONUM:HEXKEY'."); - } - String[] split = sessionKey.split(":"); - byte algorithm = (byte) Integer.parseInt(split[0]); - byte[] key = HexUtil.hexToBytes(split[1]); - - try { - decrypt.withSessionKey(new SessionKey(algorithm, key)); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - throw new SOPGPException.UnsupportedOption(String.format(ERROR_UNSUPPORTED_OPTION, "--with-session-key"), unsupportedOption); - } - } - } - - private void setWithPasswords(List withPassword, Decrypt decrypt) { - for (String password : withPassword) { - try { - decrypt.withPassword(password); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - throw new SOPGPException.UnsupportedOption(String.format(ERROR_UNSUPPORTED_OPTION, "--with-password"), unsupportedOption); - } - } - } - - private void setNotAfter(String notAfter, Decrypt decrypt) { - Date notAfterDate = DateParser.parseNotAfter(notAfter); - try { - decrypt.verifyNotAfter(notAfterDate); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - throw new SOPGPException.UnsupportedOption(String.format(ERROR_UNSUPPORTED_OPTION, "--not-after"), unsupportedOption); - } - } - - private void setNotBefore(String notBefore, Decrypt decrypt) { - Date notBeforeDate = DateParser.parseNotBefore(notBefore); - try { - decrypt.verifyNotBefore(notBeforeDate); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - throw new SOPGPException.UnsupportedOption(String.format(ERROR_UNSUPPORTED_OPTION, "--not-before"), unsupportedOption); - } - } -} diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DetachInbandSignatureAndMessageCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DetachInbandSignatureAndMessageCmd.java deleted file mode 100644 index 77471681..00000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DetachInbandSignatureAndMessageCmd.java +++ /dev/null @@ -1,55 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; - -import picocli.CommandLine; -import sop.Signatures; -import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; -import sop.operation.DetachInbandSignatureAndMessage; - -@CommandLine.Command(name = "detach-inband-signature-and-message", - description = "Split a clearsigned message", - exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) -public class DetachInbandSignatureAndMessageCmd implements Runnable { - - @CommandLine.Option( - names = {"--signatures-out"}, - description = "Destination to which a detached signatures block will be written", - paramLabel = "SIGNATURES") - File signaturesOut; - - @CommandLine.Option(names = "--no-armor", - description = "ASCII armor the output", - negatable = true) - boolean armor = true; - - @Override - public void run() { - if (signaturesOut == null) { - throw new SOPGPException.MissingArg("--signatures-out is required."); - } - - DetachInbandSignatureAndMessage detach = SopCLI.getSop().detachInbandSignatureAndMessage(); - if (!armor) { - detach.noArmor(); - } - - try { - Signatures signatures = detach - .message(System.in).writeTo(System.out); - if (!signaturesOut.createNewFile()) { - throw new SOPGPException.OutputExists("Destination of --signatures-out already exists."); - } - signatures.writeTo(new FileOutputStream(signaturesOut)); - } catch (IOException e) { - throw new RuntimeException(e); - } - } -} diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/EncryptCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/EncryptCmd.java deleted file mode 100644 index d1ee253c..00000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/EncryptCmd.java +++ /dev/null @@ -1,119 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -import picocli.CommandLine; -import sop.Ready; -import sop.cli.picocli.SopCLI; -import sop.enums.EncryptAs; -import sop.exception.SOPGPException; -import sop.operation.Encrypt; - -@CommandLine.Command(name = "encrypt", - description = "Encrypt a message from standard input", - exitCodeOnInvalidInput = 37) -public class EncryptCmd implements Runnable { - - @CommandLine.Option(names = "--no-armor", - description = "ASCII armor the output", - negatable = true) - boolean armor = true; - - @CommandLine.Option(names = {"--as"}, - description = "Type of the input data. Defaults to 'binary'", - paramLabel = "{binary|text|mime}") - EncryptAs type; - - @CommandLine.Option(names = "--with-password", - description = "Encrypt the message with a password", - paramLabel = "PASSWORD") - List withPassword = new ArrayList<>(); - - @CommandLine.Option(names = "--sign-with", - description = "Sign the output with a private key", - paramLabel = "KEY") - List signWith = new ArrayList<>(); - - @CommandLine.Parameters(description = "Certificates the message gets encrypted to", - index = "0..*", - paramLabel = "CERTS") - List certs = new ArrayList<>(); - - @Override - public void run() { - Encrypt encrypt = SopCLI.getSop().encrypt(); - if (type != null) { - try { - encrypt.mode(type); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - throw new SOPGPException.UnsupportedOption("Unsupported option '--as'.", unsupportedOption); - } - } - - if (withPassword.isEmpty() && certs.isEmpty()) { - throw new SOPGPException.MissingArg("At least one password or cert file required for encryption."); - } - - for (String password : withPassword) { - try { - encrypt.withPassword(password); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - throw new SOPGPException.UnsupportedOption("Unsupported option '--with-password'.", unsupportedOption); - } - } - - for (File keyFile : signWith) { - try (FileInputStream keyIn = new FileInputStream(keyFile)) { - encrypt.signWith(keyIn); - } catch (FileNotFoundException e) { - throw new SOPGPException.MissingInput("Key file " + keyFile.getAbsolutePath() + " not found.", e); - } catch (IOException e) { - throw new RuntimeException(e); - } catch (SOPGPException.KeyIsProtected keyIsProtected) { - throw new SOPGPException.KeyIsProtected("Key from " + keyFile.getAbsolutePath() + " is password protected.", keyIsProtected); - } catch (SOPGPException.UnsupportedAsymmetricAlgo unsupportedAsymmetricAlgo) { - throw new SOPGPException.UnsupportedAsymmetricAlgo("Key from " + keyFile.getAbsolutePath() + " has unsupported asymmetric algorithm.", unsupportedAsymmetricAlgo); - } catch (SOPGPException.CertCannotSign certCannotSign) { - throw new RuntimeException("Key from " + keyFile.getAbsolutePath() + " cannot sign.", certCannotSign); - } catch (SOPGPException.BadData badData) { - throw new SOPGPException.BadData("Key file " + keyFile.getAbsolutePath() + " does not contain a valid OpenPGP private key.", badData); - } - } - - for (File certFile : certs) { - try (FileInputStream certIn = new FileInputStream(certFile)) { - encrypt.withCert(certIn); - } catch (FileNotFoundException e) { - throw new SOPGPException.MissingInput("Certificate file " + certFile.getAbsolutePath() + " not found.", e); - } catch (IOException e) { - throw new RuntimeException(e); - } catch (SOPGPException.UnsupportedAsymmetricAlgo unsupportedAsymmetricAlgo) { - throw new SOPGPException.UnsupportedAsymmetricAlgo("Certificate from " + certFile.getAbsolutePath() + " has unsupported asymmetric algorithm.", unsupportedAsymmetricAlgo); - } catch (SOPGPException.CertCannotEncrypt certCannotEncrypt) { - throw new SOPGPException.CertCannotEncrypt("Certificate from " + certFile.getAbsolutePath() + " is not capable of encryption.", certCannotEncrypt); - } catch (SOPGPException.BadData badData) { - throw new SOPGPException.BadData("Certificate file " + certFile.getAbsolutePath() + " does not contain a valid OpenPGP certificate.", badData); - } - } - - if (!armor) { - encrypt.noArmor(); - } - - try { - Ready ready = encrypt.plaintext(System.in); - ready.writeTo(System.out); - } catch (IOException e) { - throw new RuntimeException(e); - } - } -} diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ExtractCertCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ExtractCertCmd.java deleted file mode 100644 index 59656e65..00000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ExtractCertCmd.java +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import java.io.IOException; - -import picocli.CommandLine; -import sop.Ready; -import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; -import sop.operation.ExtractCert; - -@CommandLine.Command(name = "extract-cert", - description = "Extract a public key certificate from a secret key from standard input", - exitCodeOnInvalidInput = 37) -public class ExtractCertCmd implements Runnable { - - @CommandLine.Option(names = "--no-armor", - description = "ASCII armor the output", - negatable = true) - boolean armor = true; - - @Override - public void run() { - ExtractCert extractCert = SopCLI.getSop().extractCert(); - if (!armor) { - extractCert.noArmor(); - } - - try { - Ready ready = extractCert.key(System.in); - ready.writeTo(System.out); - } catch (IOException e) { - throw new RuntimeException(e); - } catch (SOPGPException.BadData badData) { - throw new SOPGPException.BadData("Standard Input does not contain valid OpenPGP private key material.", badData); - } - } -} diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/GenerateKeyCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/GenerateKeyCmd.java deleted file mode 100644 index f97fcfa0..00000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/GenerateKeyCmd.java +++ /dev/null @@ -1,59 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -import picocli.CommandLine; -import sop.Ready; -import sop.cli.picocli.Print; -import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; -import sop.operation.GenerateKey; - -@CommandLine.Command(name = "generate-key", - description = "Generate a secret key", - exitCodeOnInvalidInput = 37) -public class GenerateKeyCmd implements Runnable { - - @CommandLine.Option(names = "--no-armor", - description = "ASCII armor the output", - negatable = true) - boolean armor = true; - - @CommandLine.Parameters(description = "User-ID, eg. \"Alice \"") - List userId = new ArrayList<>(); - - @Override - public void run() { - GenerateKey generateKey = SopCLI.getSop().generateKey(); - for (String userId : userId) { - generateKey.userId(userId); - } - - if (!armor) { - generateKey.noArmor(); - } - - try { - Ready ready = generateKey.generate(); - ready.writeTo(System.out); - } catch (SOPGPException.MissingArg missingArg) { - Print.errln("Missing argument."); - Print.trace(missingArg); - System.exit(missingArg.getExitCode()); - } catch (SOPGPException.UnsupportedAsymmetricAlgo unsupportedAsymmetricAlgo) { - Print.errln("Unsupported asymmetric algorithm."); - Print.trace(unsupportedAsymmetricAlgo); - System.exit(unsupportedAsymmetricAlgo.getExitCode()); - } catch (IOException e) { - Print.errln("IO Error."); - Print.trace(e); - System.exit(1); - } - } -} diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/SignCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/SignCmd.java deleted file mode 100644 index 961869ce..00000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/SignCmd.java +++ /dev/null @@ -1,98 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -import picocli.CommandLine; -import sop.Ready; -import sop.cli.picocli.Print; -import sop.cli.picocli.SopCLI; -import sop.enums.SignAs; -import sop.exception.SOPGPException; -import sop.operation.Sign; - -@CommandLine.Command(name = "sign", - description = "Create a detached signature on the data from standard input", - exitCodeOnInvalidInput = 37) -public class SignCmd implements Runnable { - - @CommandLine.Option(names = "--no-armor", - description = "ASCII armor the output", - negatable = true) - boolean armor = true; - - @CommandLine.Option(names = "--as", description = "Defaults to 'binary'. If '--as=text' and the input data is not valid UTF-8, sign fails with return code 53.", - paramLabel = "{binary|text}") - SignAs type; - - @CommandLine.Parameters(description = "Secret keys used for signing", - paramLabel = "KEY") - List secretKeyFile = new ArrayList<>(); - - @Override - public void run() { - Sign sign = SopCLI.getSop().sign(); - - if (type != null) { - try { - sign.mode(type); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - Print.errln("Unsupported option '--as'"); - Print.trace(unsupportedOption); - System.exit(unsupportedOption.getExitCode()); - } - } - - if (secretKeyFile.isEmpty()) { - Print.errln("Missing required parameter 'KEY'."); - System.exit(19); - } - - for (File keyFile : secretKeyFile) { - try (FileInputStream keyIn = new FileInputStream(keyFile)) { - sign.key(keyIn); - } catch (FileNotFoundException e) { - Print.errln("File " + keyFile.getAbsolutePath() + " does not exist."); - Print.trace(e); - System.exit(1); - } catch (IOException e) { - Print.errln("Cannot access file " + keyFile.getAbsolutePath()); - Print.trace(e); - System.exit(1); - } catch (SOPGPException.KeyIsProtected e) { - Print.errln("Key " + keyFile.getName() + " is password protected."); - Print.trace(e); - System.exit(1); - } catch (SOPGPException.BadData badData) { - Print.errln("Bad data in key file " + keyFile.getAbsolutePath() + ":"); - Print.trace(badData); - System.exit(badData.getExitCode()); - } - } - - if (!armor) { - sign.noArmor(); - } - - try { - Ready ready = sign.data(System.in); - ready.writeTo(System.out); - } catch (IOException e) { - Print.errln("IO Error."); - Print.trace(e); - System.exit(1); - } catch (SOPGPException.ExpectedText expectedText) { - Print.errln("Expected text input, but got binary data."); - Print.trace(expectedText); - System.exit(expectedText.getExitCode()); - } - } -} diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VerifyCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VerifyCmd.java deleted file mode 100644 index d731b25a..00000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VerifyCmd.java +++ /dev/null @@ -1,132 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -import picocli.CommandLine; -import sop.Verification; -import sop.cli.picocli.DateParser; -import sop.cli.picocli.Print; -import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; -import sop.operation.Verify; - -@CommandLine.Command(name = "verify", - description = "Verify a detached signature over the data from standard input", - exitCodeOnInvalidInput = 37) -public class VerifyCmd implements Runnable { - - @CommandLine.Parameters(index = "0", - description = "Detached signature", - paramLabel = "SIGNATURE") - File signature; - - @CommandLine.Parameters(index = "1..*", - arity = "1..*", - description = "Public key certificates", - paramLabel = "CERT") - List certificates = new ArrayList<>(); - - @CommandLine.Option(names = {"--not-before"}, - description = "ISO-8601 formatted UTC date (eg. '2020-11-23T16:35Z)\n" + - "Reject signatures with a creation date not in range.\n" + - "Defaults to beginning of time (\"-\").", - paramLabel = "DATE") - String notBefore = "-"; - - @CommandLine.Option(names = {"--not-after"}, - description = "ISO-8601 formatted UTC date (eg. '2020-11-23T16:35Z)\n" + - "Reject signatures with a creation date not in range.\n" + - "Defaults to current system time (\"now\").\n" + - "Accepts special value \"-\" for end of time.", - paramLabel = "DATE") - String notAfter = "now"; - - @Override - public void run() { - Verify verify = SopCLI.getSop().verify(); - if (notAfter != null) { - try { - verify.notAfter(DateParser.parseNotAfter(notAfter)); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - Print.errln("Unsupported option '--not-after'."); - Print.trace(unsupportedOption); - System.exit(unsupportedOption.getExitCode()); - } - } - if (notBefore != null) { - try { - verify.notBefore(DateParser.parseNotBefore(notBefore)); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - Print.errln("Unsupported option '--not-before'."); - Print.trace(unsupportedOption); - System.exit(unsupportedOption.getExitCode()); - } - } - - for (File certFile : certificates) { - try (FileInputStream certIn = new FileInputStream(certFile)) { - verify.cert(certIn); - } catch (FileNotFoundException fileNotFoundException) { - Print.errln("Certificate file " + certFile.getAbsolutePath() + " not found."); - - Print.trace(fileNotFoundException); - System.exit(1); - } catch (IOException ioException) { - Print.errln("IO Error."); - Print.trace(ioException); - System.exit(1); - } catch (SOPGPException.BadData badData) { - Print.errln("Certificate file " + certFile.getAbsolutePath() + " appears to not contain a valid OpenPGP certificate."); - Print.trace(badData); - System.exit(badData.getExitCode()); - } - } - - if (signature != null) { - try (FileInputStream sigIn = new FileInputStream(signature)) { - verify.signatures(sigIn); - } catch (FileNotFoundException e) { - Print.errln("Signature file " + signature.getAbsolutePath() + " does not exist."); - Print.trace(e); - System.exit(1); - } catch (IOException e) { - Print.errln("IO Error."); - Print.trace(e); - System.exit(1); - } catch (SOPGPException.BadData badData) { - Print.errln("File " + signature.getAbsolutePath() + " does not contain a valid OpenPGP signature."); - Print.trace(badData); - System.exit(badData.getExitCode()); - } - } - - List verifications = null; - try { - verifications = verify.data(System.in); - } catch (SOPGPException.NoSignature e) { - Print.errln("No verifiable signature found."); - Print.trace(e); - System.exit(e.getExitCode()); - } catch (IOException ioException) { - Print.errln("IO Error."); - Print.trace(ioException); - System.exit(1); - } catch (SOPGPException.BadData badData) { - Print.errln("Standard Input appears not to contain a valid OpenPGP message."); - Print.trace(badData); - System.exit(badData.getExitCode()); - } - for (Verification verification : verifications) { - Print.outln(verification.toString()); - } - } -} diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VersionCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VersionCmd.java deleted file mode 100644 index 0d5da1a6..00000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VersionCmd.java +++ /dev/null @@ -1,22 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import picocli.CommandLine; -import sop.cli.picocli.Print; -import sop.cli.picocli.SopCLI; -import sop.operation.Version; - -@CommandLine.Command(name = "version", description = "Display version information about the tool", - exitCodeOnInvalidInput = 37) -public class VersionCmd implements Runnable { - - @Override - public void run() { - Version version = SopCLI.getSop().version(); - - Print.outln(version.getName() + " " + version.getVersion()); - } -} diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/package-info.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/package-info.java deleted file mode 100644 index fc6aefda..00000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Subcommands of the PGPainless SOP. - */ -package sop.cli.picocli.commands; diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/package-info.java b/sop-java-picocli/src/main/java/sop/cli/picocli/package-info.java deleted file mode 100644 index 83f426d6..00000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Implementation of the Stateless OpenPGP Command Line Interface using Picocli. - */ -package sop.cli.picocli; diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/DateParserTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/DateParserTest.java deleted file mode 100644 index 5c7def50..00000000 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/DateParserTest.java +++ /dev/null @@ -1,49 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import java.util.Date; - -import org.junit.jupiter.api.Test; -import sop.util.UTCUtil; - -public class DateParserTest { - - @Test - public void parseNotAfterDashReturnsEndOfTime() { - assertEquals(DateParser.END_OF_TIME, DateParser.parseNotAfter("-")); - } - - @Test - public void parseNotBeforeDashReturnsBeginningOfTime() { - assertEquals(DateParser.BEGINNING_OF_TIME, DateParser.parseNotBefore("-")); - } - - @Test - public void parseNotAfterNowReturnsNow() { - assertEquals(new Date().getTime(), DateParser.parseNotAfter("now").getTime(), 1000); - } - - @Test - public void parseNotBeforeNowReturnsNow() { - assertEquals(new Date().getTime(), DateParser.parseNotBefore("now").getTime(), 1000); - } - - @Test - public void parseNotAfterTimestamp() { - String timestamp = "2019-10-24T23:48:29Z"; - Date date = DateParser.parseNotAfter(timestamp); - assertEquals(timestamp, UTCUtil.formatUTCDate(date)); - } - - @Test - public void parseNotBeforeTimestamp() { - String timestamp = "2019-10-29T18:36:45Z"; - Date date = DateParser.parseNotBefore(timestamp); - assertEquals(timestamp, UTCUtil.formatUTCDate(date)); - } -} diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/FileUtilTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/FileUtilTest.java deleted file mode 100644 index eeb4589d..00000000 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/FileUtilTest.java +++ /dev/null @@ -1,123 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.nio.file.Files; - -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import sop.exception.SOPGPException; - -public class FileUtilTest { - - @BeforeAll - public static void setup() { - FileUtil.setEnvironmentVariableResolver(new FileUtil.EnvironmentVariableResolver() { - @Override - public String resolveEnvironmentVariable(String name) { - if (name.equals("test123")) { - return "test321"; - } - return null; - } - }); - } - - @Test - public void getFile_ThrowsForNull() { - assertThrows(NullPointerException.class, () -> FileUtil.getFile(null)); - } - - @Test - public void getFile_prfxEnvAlreadyExists() throws IOException { - File tempFile = new File("@ENV:test"); - tempFile.createNewFile(); - tempFile.deleteOnExit(); - - assertThrows(SOPGPException.AmbiguousInput.class, () -> FileUtil.getFile("@ENV:test")); - } - - @Test - public void getFile_EnvironmentVariable() { - File file = FileUtil.getFile("@ENV:test123"); - assertEquals("test321", file.getName()); - } - - @Test - public void getFile_nonExistentEnvVariable() { - assertThrows(IllegalArgumentException.class, () -> FileUtil.getFile("@ENV:INVALID")); - } - - @Test - public void getFile_prfxFdAlreadyExists() throws IOException { - File tempFile = new File("@FD:1"); - tempFile.createNewFile(); - tempFile.deleteOnExit(); - - assertThrows(SOPGPException.AmbiguousInput.class, () -> FileUtil.getFile("@FD:1")); - } - - @Test - public void getFile_prfxFdNotSupported() { - assertThrows(IllegalArgumentException.class, () -> FileUtil.getFile("@FD:2")); - } - - @Test - public void createNewFileOrThrow_throwsForNull() { - assertThrows(NullPointerException.class, () -> FileUtil.createNewFileOrThrow(null)); - } - - @Test - public void createNewFileOrThrow_success() throws IOException { - File dir = Files.createTempDirectory("test").toFile(); - dir.deleteOnExit(); - File file = new File(dir, "file"); - - assertFalse(file.exists()); - FileUtil.createNewFileOrThrow(file); - assertTrue(file.exists()); - } - - @Test - public void createNewFileOrThrow_alreadyExists() throws IOException { - File dir = Files.createTempDirectory("test").toFile(); - dir.deleteOnExit(); - File file = new File(dir, "file"); - - FileUtil.createNewFileOrThrow(file); - assertTrue(file.exists()); - assertThrows(SOPGPException.OutputExists.class, () -> FileUtil.createNewFileOrThrow(file)); - } - - @Test - public void getFileInputStream_success() throws IOException { - File dir = Files.createTempDirectory("test").toFile(); - dir.deleteOnExit(); - File file = new File(dir, "file"); - - FileUtil.createNewFileOrThrow(file); - FileInputStream inputStream = FileUtil.getFileInputStream(file.getAbsolutePath()); - assertNotNull(inputStream); - } - - @Test - public void getFileInputStream_fileNotFound() throws IOException { - File dir = Files.createTempDirectory("test").toFile(); - dir.deleteOnExit(); - File file = new File(dir, "file"); - - assertThrows(SOPGPException.MissingInput.class, - () -> FileUtil.getFileInputStream(file.getAbsolutePath())); - } -} diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/SOPTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/SOPTest.java deleted file mode 100644 index fbf4cfa7..00000000 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/SOPTest.java +++ /dev/null @@ -1,31 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli; - -import static org.mockito.Mockito.mock; - -import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; -import org.junit.jupiter.api.Test; -import sop.SOP; - -public class SOPTest { - - @Test - @ExpectSystemExitWithStatus(69) - public void assertExitOnInvalidSubcommand() { - SOP sop = mock(SOP.class); - SopCLI.setSopInstance(sop); - - SopCLI.main(new String[] {"invalid"}); - } - - @Test - @ExpectSystemExitWithStatus(1) - public void assertThrowsIfNoSOPBackendSet() { - SopCLI.SOP_INSTANCE = null; - // At this point, no SOP backend is set, so an InvalidStateException triggers exit(1) - SopCLI.main(new String[] {"armor"}); - } -} diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/ArmorCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/ArmorCmdTest.java deleted file mode 100644 index 01aaa9a5..00000000 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/ArmorCmdTest.java +++ /dev/null @@ -1,101 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; -import com.ginsberg.junit.exit.FailOnSystemExit; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import sop.Ready; -import sop.SOP; -import sop.cli.picocli.SopCLI; -import sop.enums.ArmorLabel; -import sop.exception.SOPGPException; -import sop.operation.Armor; - -public class ArmorCmdTest { - - private Armor armor; - private SOP sop; - - @BeforeEach - public void mockComponents() throws SOPGPException.BadData { - armor = mock(Armor.class); - sop = mock(SOP.class); - when(sop.armor()).thenReturn(armor); - when(armor.data((InputStream) any())).thenReturn(nopReady()); - - SopCLI.setSopInstance(sop); - } - - @Test - public void assertLabelIsNotCalledByDefault() throws SOPGPException.UnsupportedOption { - SopCLI.main(new String[] {"armor"}); - verify(armor, never()).label(any()); - } - - @Test - public void assertLabelIsCalledWhenFlaggedWithArgument() throws SOPGPException.UnsupportedOption { - for (ArmorLabel label : ArmorLabel.values()) { - SopCLI.main(new String[] {"armor", "--label", label.name()}); - verify(armor, times(1)).label(label); - } - } - - @Test - public void assertDataIsAlwaysCalled() throws SOPGPException.BadData { - SopCLI.main(new String[] {"armor"}); - verify(armor, times(1)).data((InputStream) any()); - } - - @Test - @ExpectSystemExitWithStatus(37) - public void assertThrowsForInvalidLabel() { - SopCLI.main(new String[] {"armor", "--label", "Invalid"}); - } - - @Test - @ExpectSystemExitWithStatus(37) - public void ifLabelsUnsupportedExit37() throws SOPGPException.UnsupportedOption { - when(armor.label(any())).thenThrow(new SOPGPException.UnsupportedOption("Custom Armor labels are not supported.")); - - SopCLI.main(new String[] {"armor", "--label", "Sig"}); - } - - @Test - @ExpectSystemExitWithStatus(41) - public void ifBadDataExit41() throws SOPGPException.BadData { - when(armor.data((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); - - SopCLI.main(new String[] {"armor"}); - } - - @Test - @FailOnSystemExit - public void ifNoErrorsNoExit() { - when(sop.armor()).thenReturn(armor); - - SopCLI.main(new String[] {"armor"}); - } - - private static Ready nopReady() { - return new Ready() { - @Override - public void writeTo(OutputStream outputStream) { - } - }; - } -} diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/DearmorCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/DearmorCmdTest.java deleted file mode 100644 index aaad201b..00000000 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/DearmorCmdTest.java +++ /dev/null @@ -1,61 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import sop.Ready; -import sop.SOP; -import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; -import sop.operation.Dearmor; - -public class DearmorCmdTest { - - private SOP sop; - private Dearmor dearmor; - - @BeforeEach - public void mockComponents() throws IOException, SOPGPException.BadData { - sop = mock(SOP.class); - dearmor = mock(Dearmor.class); - when(dearmor.data((InputStream) any())).thenReturn(nopReady()); - when(sop.dearmor()).thenReturn(dearmor); - - SopCLI.setSopInstance(sop); - } - - private static Ready nopReady() { - return new Ready() { - @Override - public void writeTo(OutputStream outputStream) { - } - }; - } - - @Test - public void assertDataIsCalled() throws IOException, SOPGPException.BadData { - SopCLI.main(new String[] {"dearmor"}); - verify(dearmor, times(1)).data((InputStream) any()); - } - - @Test - @ExpectSystemExitWithStatus(41) - public void assertBadDataCausesExit41() throws IOException, SOPGPException.BadData { - when(dearmor.data((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException("invalid armor"))); - SopCLI.main(new String[] {"dearmor"}); - } -} diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/DecryptCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/DecryptCmdTest.java deleted file mode 100644 index 507b6723..00000000 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/DecryptCmdTest.java +++ /dev/null @@ -1,344 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.io.BufferedReader; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Collections; -import java.util.Date; - -import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentMatcher; -import org.mockito.ArgumentMatchers; -import sop.DecryptionResult; -import sop.ReadyWithResult; -import sop.SOP; -import sop.SessionKey; -import sop.Verification; -import sop.cli.picocli.DateParser; -import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; -import sop.operation.Decrypt; -import sop.util.HexUtil; -import sop.util.UTCUtil; - -public class DecryptCmdTest { - - private Decrypt decrypt; - - @BeforeEach - public void mockComponents() throws SOPGPException.UnsupportedOption, SOPGPException.MissingArg, SOPGPException.BadData, SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.PasswordNotHumanReadable, SOPGPException.CannotDecrypt { - SOP sop = mock(SOP.class); - decrypt = mock(Decrypt.class); - - when(decrypt.verifyNotAfter(any())).thenReturn(decrypt); - when(decrypt.verifyNotBefore(any())).thenReturn(decrypt); - when(decrypt.withPassword(any())).thenReturn(decrypt); - when(decrypt.withSessionKey(any())).thenReturn(decrypt); - when(decrypt.withKey((InputStream) any())).thenReturn(decrypt); - when(decrypt.ciphertext((InputStream) any())).thenReturn(nopReadyWithResult()); - - when(sop.decrypt()).thenReturn(decrypt); - - SopCLI.setSopInstance(sop); - } - - private static ReadyWithResult nopReadyWithResult() { - return new ReadyWithResult() { - @Override - public DecryptionResult writeTo(OutputStream outputStream) { - return new DecryptionResult(null, Collections.emptyList()); - } - }; - } - - @Test - @ExpectSystemExitWithStatus(19) - public void missingArgumentsExceptionCausesExit19() throws SOPGPException.MissingArg, SOPGPException.BadData, SOPGPException.CannotDecrypt { - when(decrypt.ciphertext((InputStream) any())).thenThrow(new SOPGPException.MissingArg("Missing arguments.")); - SopCLI.main(new String[] {"decrypt"}); - } - - @Test - @ExpectSystemExitWithStatus(41) - public void badDataExceptionCausesExit41() throws SOPGPException.MissingArg, SOPGPException.BadData, SOPGPException.CannotDecrypt { - when(decrypt.ciphertext((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); - SopCLI.main(new String[] {"decrypt"}); - } - - @Test - @ExpectSystemExitWithStatus(31) - public void assertNotHumanReadablePasswordCausesExit31() throws SOPGPException.PasswordNotHumanReadable, - SOPGPException.UnsupportedOption { - when(decrypt.withPassword(any())).thenThrow(new SOPGPException.PasswordNotHumanReadable()); - SopCLI.main(new String[] {"decrypt", "--with-password", "pretendThisIsNotReadable"}); - } - - @Test - public void assertWithPasswordPassesPasswordDown() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption { - SopCLI.main(new String[] {"decrypt", "--with-password", "orange"}); - verify(decrypt, times(1)).withPassword("orange"); - } - - @Test - @ExpectSystemExitWithStatus(37) - public void assertUnsupportedWithPasswordCausesExit37() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption { - when(decrypt.withPassword(any())).thenThrow(new SOPGPException.UnsupportedOption("Decrypting with password not supported.")); - SopCLI.main(new String[] {"decrypt", "--with-password", "swordfish"}); - } - - @Test - public void assertDefaultTimeRangesAreUsedIfNotOverwritten() throws SOPGPException.UnsupportedOption { - Date now = new Date(); - SopCLI.main(new String[] {"decrypt"}); - verify(decrypt, times(1)).verifyNotBefore(DateParser.BEGINNING_OF_TIME); - verify(decrypt, times(1)).verifyNotAfter( - ArgumentMatchers.argThat(argument -> { - // allow 1 second difference - return Math.abs(now.getTime() - argument.getTime()) <= 1000; - })); - } - - @Test - public void assertVerifyNotAfterAndBeforeDashResultsInMaxTimeRange() throws SOPGPException.UnsupportedOption { - SopCLI.main(new String[] {"decrypt", "--not-before", "-", "--not-after", "-"}); - verify(decrypt, times(1)).verifyNotBefore(DateParser.BEGINNING_OF_TIME); - verify(decrypt, times(1)).verifyNotAfter(DateParser.END_OF_TIME); - } - - @Test - public void assertVerifyNotAfterAndBeforeNowResultsInMinTimeRange() throws SOPGPException.UnsupportedOption { - Date now = new Date(); - ArgumentMatcher isMaxOneSecOff = argument -> { - // Allow less than 1 second difference - return Math.abs(now.getTime() - argument.getTime()) <= 1000; - }; - - SopCLI.main(new String[] {"decrypt", "--not-before", "now", "--not-after", "now"}); - verify(decrypt, times(1)).verifyNotAfter(ArgumentMatchers.argThat(isMaxOneSecOff)); - verify(decrypt, times(1)).verifyNotBefore(ArgumentMatchers.argThat(isMaxOneSecOff)); - } - - @Test - @ExpectSystemExitWithStatus(1) - public void assertMalformedDateInNotBeforeCausesExit1() { - // ParserException causes exit(1) - SopCLI.main(new String[] {"decrypt", "--not-before", "invalid"}); - } - - @Test - @ExpectSystemExitWithStatus(1) - public void assertMalformedDateInNotAfterCausesExit1() { - // ParserException causes exit(1) - SopCLI.main(new String[] {"decrypt", "--not-after", "invalid"}); - } - - @Test - @ExpectSystemExitWithStatus(37) - public void assertUnsupportedNotAfterCausesExit37() throws SOPGPException.UnsupportedOption { - when(decrypt.verifyNotAfter(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting upper signature date boundary not supported.")); - SopCLI.main(new String[] {"decrypt", "--not-after", "now"}); - } - - @Test - @ExpectSystemExitWithStatus(37) - public void assertUnsupportedNotBeforeCausesExit37() throws SOPGPException.UnsupportedOption { - when(decrypt.verifyNotBefore(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting lower signature date boundary not supported.")); - SopCLI.main(new String[] {"decrypt", "--not-before", "now"}); - } - - @Test - @ExpectSystemExitWithStatus(59) - public void assertExistingSessionKeyOutFileCausesExit59() throws IOException { - File tempFile = File.createTempFile("existing-session-key-", ".tmp"); - tempFile.deleteOnExit(); - SopCLI.main(new String[] {"decrypt", "--session-key-out", tempFile.getAbsolutePath()}); - } - - @Test - @ExpectSystemExitWithStatus(37) - public void assertWhenSessionKeyCannotBeExtractedExit37() throws IOException { - Path tempDir = Files.createTempDirectory("session-key-out-dir"); - File tempFile = new File(tempDir.toFile(), "session-key"); - tempFile.deleteOnExit(); - SopCLI.main(new String[] {"decrypt", "--session-key-out", tempFile.getAbsolutePath()}); - } - - @Test - public void assertSessionKeyIsProperlyWrittenToSessionKeyFile() throws SOPGPException.CannotDecrypt, SOPGPException.MissingArg, SOPGPException.BadData, IOException { - byte[] key = "C7CBDAF42537776F12509B5168793C26B93294E5ABDFA73224FB0177123E9137".getBytes(StandardCharsets.UTF_8); - when(decrypt.ciphertext((InputStream) any())).thenReturn(new ReadyWithResult() { - @Override - public DecryptionResult writeTo(OutputStream outputStream) { - return new DecryptionResult( - new SessionKey((byte) 9, key), - Collections.emptyList() - ); - } - }); - Path tempDir = Files.createTempDirectory("session-key-out-dir"); - File tempFile = new File(tempDir.toFile(), "session-key"); - tempFile.deleteOnExit(); - SopCLI.main(new String[] {"decrypt", "--session-key-out", tempFile.getAbsolutePath()}); - - ByteArrayOutputStream bytesInFile = new ByteArrayOutputStream(); - try (FileInputStream fileIn = new FileInputStream(tempFile)) { - byte[] buf = new byte[32]; - int read = fileIn.read(buf); - while (read != -1) { - bytesInFile.write(buf, 0, read); - read = fileIn.read(buf); - } - } - - byte[] algAndKey = new byte[key.length + 1]; - algAndKey[0] = (byte) 9; - System.arraycopy(key, 0, algAndKey, 1, key.length); - assertArrayEquals(algAndKey, bytesInFile.toByteArray()); - } - - @Test - @ExpectSystemExitWithStatus(29) - public void assertUnableToDecryptExceptionResultsInExit29() throws SOPGPException.CannotDecrypt, SOPGPException.MissingArg, SOPGPException.BadData { - when(decrypt.ciphertext((InputStream) any())).thenThrow(new SOPGPException.CannotDecrypt()); - SopCLI.main(new String[] {"decrypt"}); - } - - @Test - @ExpectSystemExitWithStatus(3) - public void assertNoSignatureExceptionCausesExit3() throws SOPGPException.CannotDecrypt, SOPGPException.MissingArg, SOPGPException.BadData { - when(decrypt.ciphertext((InputStream) any())).thenReturn(new ReadyWithResult() { - @Override - public DecryptionResult writeTo(OutputStream outputStream) throws SOPGPException.NoSignature { - throw new SOPGPException.NoSignature(); - } - }); - SopCLI.main(new String[] {"decrypt"}); - } - - @Test - @ExpectSystemExitWithStatus(41) - public void badDataInVerifyWithCausesExit41() throws IOException, SOPGPException.BadData { - when(decrypt.verifyWithCert((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); - File tempFile = File.createTempFile("verify-with-", ".tmp"); - SopCLI.main(new String[] {"decrypt", "--verify-with", tempFile.getAbsolutePath()}); - } - - @Test - @ExpectSystemExitWithStatus(61) - public void unexistentCertFileCausesExit61() { - SopCLI.main(new String[] {"decrypt", "--verify-with", "invalid"}); - } - - @Test - @ExpectSystemExitWithStatus(59) - public void existingVerifyOutCausesExit59() throws IOException { - File certFile = File.createTempFile("existing-verify-out-cert", ".asc"); - File existingVerifyOut = File.createTempFile("existing-verify-out", ".tmp"); - - SopCLI.main(new String[] {"decrypt", "--verify-out", existingVerifyOut.getAbsolutePath(), "--verify-with", certFile.getAbsolutePath()}); - } - - @Test - public void verifyOutIsProperlyWritten() throws IOException, SOPGPException.CannotDecrypt, SOPGPException.MissingArg, SOPGPException.BadData { - File certFile = File.createTempFile("verify-out-cert", ".asc"); - File verifyOut = new File(certFile.getParent(), "verify-out.txt"); - if (verifyOut.exists()) { - verifyOut.delete(); - } - verifyOut.deleteOnExit(); - Date date = UTCUtil.parseUTCDate("2021-07-11T20:58:23Z"); - when(decrypt.ciphertext((InputStream) any())).thenReturn(new ReadyWithResult() { - @Override - public DecryptionResult writeTo(OutputStream outputStream) { - return new DecryptionResult(null, Collections.singletonList( - new Verification( - date, - "1B66A707819A920925BC6777C3E0AFC0B2DFF862", - "C8CD564EBF8D7BBA90611D8D071773658BF6BF86")) - ); - } - }); - - SopCLI.main(new String[] {"decrypt", "--verify-out", verifyOut.getAbsolutePath(), "--verify-with", certFile.getAbsolutePath()}); - try (BufferedReader reader = new BufferedReader(new FileReader(verifyOut))) { - String line = reader.readLine(); - assertEquals("2021-07-11T20:58:23Z 1B66A707819A920925BC6777C3E0AFC0B2DFF862 C8CD564EBF8D7BBA90611D8D071773658BF6BF86", line); - } - } - - @Test - public void assertWithSessionKeyIsPassedDown() throws SOPGPException.UnsupportedOption { - SessionKey key1 = new SessionKey((byte) 9, HexUtil.hexToBytes("C7CBDAF42537776F12509B5168793C26B93294E5ABDFA73224FB0177123E9137")); - SessionKey key2 = new SessionKey((byte) 9, HexUtil.hexToBytes("FCA4BEAF687F48059CACC14FB019125CD57392BAB7037C707835925CBF9F7BCD")); - SopCLI.main(new String[] {"decrypt", - "--with-session-key", "9:C7CBDAF42537776F12509B5168793C26B93294E5ABDFA73224FB0177123E9137", - "--with-session-key", "9:FCA4BEAF687F48059CACC14FB019125CD57392BAB7037C707835925CBF9F7BCD"}); - verify(decrypt).withSessionKey(key1); - verify(decrypt).withSessionKey(key2); - } - - @Test - @ExpectSystemExitWithStatus(1) - public void assertMalformedSessionKeysResultInExit1() { - SopCLI.main(new String[] {"decrypt", - "--with-session-key", "C7CBDAF42537776F12509B5168793C26B93294E5ABDFA73224FB0177123E9137"}); - } - - @Test - @ExpectSystemExitWithStatus(41) - public void assertBadDataInKeysResultsInExit41() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData, IOException { - when(decrypt.withKey((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); - File tempKeyFile = File.createTempFile("key-", ".tmp"); - SopCLI.main(new String[] {"decrypt", tempKeyFile.getAbsolutePath()}); - } - - @Test - @ExpectSystemExitWithStatus(61) - public void assertKeyFileNotFoundCausesExit61() { - SopCLI.main(new String[] {"decrypt", "nonexistent-key"}); - } - - @Test - @ExpectSystemExitWithStatus(67) - public void assertProtectedKeyCausesExit67() throws IOException, SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData { - when(decrypt.withKey((InputStream) any())).thenThrow(new SOPGPException.KeyIsProtected()); - File tempKeyFile = File.createTempFile("key-", ".tmp"); - SopCLI.main(new String[] {"decrypt", tempKeyFile.getAbsolutePath()}); - } - - @Test - @ExpectSystemExitWithStatus(13) - public void assertUnsupportedAlgorithmExceptionCausesExit13() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData, IOException { - when(decrypt.withKey((InputStream) any())).thenThrow(new SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", new IOException())); - File tempKeyFile = File.createTempFile("key-", ".tmp"); - SopCLI.main(new String[] {"decrypt", tempKeyFile.getAbsolutePath()}); - } - - @Test - @ExpectSystemExitWithStatus(23) - public void verifyOutWithoutVerifyWithCausesExit23() { - SopCLI.main(new String[] {"decrypt", "--verify-out", "out.file"}); - } -} diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/EncryptCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/EncryptCmdTest.java deleted file mode 100644 index cfa8a3f6..00000000 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/EncryptCmdTest.java +++ /dev/null @@ -1,194 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import sop.Ready; -import sop.SOP; -import sop.cli.picocli.SopCLI; -import sop.enums.EncryptAs; -import sop.exception.SOPGPException; -import sop.operation.Encrypt; - -public class EncryptCmdTest { - - Encrypt encrypt; - - @BeforeEach - public void mockComponents() throws IOException { - encrypt = mock(Encrypt.class); - when(encrypt.plaintext((InputStream) any())).thenReturn(new Ready() { - @Override - public void writeTo(OutputStream outputStream) { - - } - }); - - SOP sop = mock(SOP.class); - when(sop.encrypt()).thenReturn(encrypt); - - SopCLI.setSopInstance(sop); - } - - @Test - @ExpectSystemExitWithStatus(19) - public void missingBothPasswordAndCertFileCauseExit19() { - SopCLI.main(new String[] {"encrypt", "--no-armor"}); - } - - @Test - @ExpectSystemExitWithStatus(37) - public void as_unsupportedEncryptAsCausesExit37() throws SOPGPException.UnsupportedOption { - when(encrypt.mode(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting encryption mode not supported.")); - - SopCLI.main(new String[] {"encrypt", "--as", "Binary"}); - } - - @Test - @ExpectSystemExitWithStatus(37) - public void as_invalidModeOptionCausesExit37() { - SopCLI.main(new String[] {"encrypt", "--as", "invalid"}); - } - - @Test - public void as_modeIsPassedDown() throws SOPGPException.UnsupportedOption { - for (EncryptAs mode : EncryptAs.values()) { - SopCLI.main(new String[] {"encrypt", "--as", mode.name(), "--with-password", "0rbit"}); - verify(encrypt, times(1)).mode(mode); - } - } - - @Test - @ExpectSystemExitWithStatus(31) - public void withPassword_notHumanReadablePasswordCausesExit31() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption { - when(encrypt.withPassword("pretendThisIsNotReadable")).thenThrow(new SOPGPException.PasswordNotHumanReadable()); - - SopCLI.main(new String[] {"encrypt", "--with-password", "pretendThisIsNotReadable"}); - } - - @Test - @ExpectSystemExitWithStatus(37) - public void withPassword_unsupportedWithPasswordCausesExit37() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption { - when(encrypt.withPassword(any())).thenThrow(new SOPGPException.UnsupportedOption("Encrypting with password not supported.")); - - SopCLI.main(new String[] {"encrypt", "--with-password", "orange"}); - } - - @Test - public void signWith_multipleTimesGetPassedDown() throws IOException, SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotSign, SOPGPException.BadData { - File keyFile1 = File.createTempFile("sign-with-1-", ".asc"); - File keyFile2 = File.createTempFile("sign-with-2-", ".asc"); - - SopCLI.main(new String[] {"encrypt", "--with-password", "password", "--sign-with", keyFile1.getAbsolutePath(), "--sign-with", keyFile2.getAbsolutePath()}); - verify(encrypt, times(2)).signWith((InputStream) any()); - } - - @Test - @ExpectSystemExitWithStatus(61) - public void signWith_nonExistentKeyFileCausesExit61() { - SopCLI.main(new String[] {"encrypt", "--with-password", "admin", "--sign-with", "nonExistent.asc"}); - } - - @Test - @ExpectSystemExitWithStatus(67) - public void signWith_keyIsProtectedCausesExit67() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotSign, SOPGPException.BadData, IOException { - when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.KeyIsProtected()); - File keyFile = File.createTempFile("sign-with", ".asc"); - SopCLI.main(new String[] {"encrypt", "--sign-with", keyFile.getAbsolutePath(), "--with-password", "starship"}); - } - - @Test - @ExpectSystemExitWithStatus(13) - public void signWith_unsupportedAsymmetricAlgoCausesExit13() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotSign, SOPGPException.BadData, IOException { - when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", new Exception())); - File keyFile = File.createTempFile("sign-with", ".asc"); - SopCLI.main(new String[] {"encrypt", "--with-password", "123456", "--sign-with", keyFile.getAbsolutePath()}); - } - - @Test - @ExpectSystemExitWithStatus(1) - public void signWith_certCannotSignCausesExit1() throws IOException, SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotSign, SOPGPException.BadData { - when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.CertCannotSign()); - File keyFile = File.createTempFile("sign-with", ".asc"); - SopCLI.main(new String[] {"encrypt", "--with-password", "dragon", "--sign-with", keyFile.getAbsolutePath()}); - } - - @Test - @ExpectSystemExitWithStatus(41) - public void signWith_badDataCausesExit41() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotSign, SOPGPException.BadData, IOException { - when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); - File keyFile = File.createTempFile("sign-with", ".asc"); - SopCLI.main(new String[] {"encrypt", "--with-password", "orange", "--sign-with", keyFile.getAbsolutePath()}); - } - - @Test - @ExpectSystemExitWithStatus(61) - public void cert_nonExistentCertFileCausesExit61() { - SopCLI.main(new String[] {"encrypt", "invalid.asc"}); - } - - @Test - @ExpectSystemExitWithStatus(13) - public void cert_unsupportedAsymmetricAlgorithmCausesExit13() throws IOException, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotEncrypt, SOPGPException.BadData { - when(encrypt.withCert((InputStream) any())).thenThrow(new SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", new Exception())); - File certFile = File.createTempFile("cert", ".asc"); - SopCLI.main(new String[] {"encrypt", certFile.getAbsolutePath()}); - } - - @Test - @ExpectSystemExitWithStatus(17) - public void cert_certCannotEncryptCausesExit17() throws IOException, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotEncrypt, SOPGPException.BadData { - when(encrypt.withCert((InputStream) any())).thenThrow(new SOPGPException.CertCannotEncrypt("Certificate cannot encrypt.", new Exception())); - File certFile = File.createTempFile("cert", ".asc"); - SopCLI.main(new String[] {"encrypt", certFile.getAbsolutePath()}); - } - - @Test - @ExpectSystemExitWithStatus(41) - public void cert_badDataCausesExit41() throws IOException, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotEncrypt, SOPGPException.BadData { - when(encrypt.withCert((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); - File certFile = File.createTempFile("cert", ".asc"); - SopCLI.main(new String[] {"encrypt", certFile.getAbsolutePath()}); - } - - @Test - public void noArmor_notCalledByDefault() { - SopCLI.main(new String[] {"encrypt", "--with-password", "clownfish"}); - verify(encrypt, never()).noArmor(); - } - - @Test - public void noArmor_callGetsPassedDown() { - SopCLI.main(new String[] {"encrypt", "--with-password", "monkey", "--no-armor"}); - verify(encrypt, times(1)).noArmor(); - } - - @Test - @ExpectSystemExitWithStatus(1) - public void writeTo_ioExceptionCausesExit1() throws IOException { - when(encrypt.plaintext((InputStream) any())).thenReturn(new Ready() { - @Override - public void writeTo(OutputStream outputStream) throws IOException { - throw new IOException(); - } - }); - - SopCLI.main(new String[] {"encrypt", "--with-password", "wildcat"}); - } -} diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/ExtractCertCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/ExtractCertCmdTest.java deleted file mode 100644 index 382fe300..00000000 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/ExtractCertCmdTest.java +++ /dev/null @@ -1,76 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import sop.Ready; -import sop.SOP; -import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; -import sop.operation.ExtractCert; - -public class ExtractCertCmdTest { - - ExtractCert extractCert; - - @BeforeEach - public void mockComponents() throws IOException, SOPGPException.BadData { - extractCert = mock(ExtractCert.class); - when(extractCert.key((InputStream) any())).thenReturn(new Ready() { - @Override - public void writeTo(OutputStream outputStream) { - } - }); - - SOP sop = mock(SOP.class); - when(sop.extractCert()).thenReturn(extractCert); - - SopCLI.setSopInstance(sop); - } - - @Test - public void noArmor_notCalledByDefault() { - SopCLI.main(new String[] {"extract-cert"}); - verify(extractCert, never()).noArmor(); - } - - @Test - public void noArmor_passedDown() { - SopCLI.main(new String[] {"extract-cert", "--no-armor"}); - verify(extractCert, times(1)).noArmor(); - } - - @Test - @ExpectSystemExitWithStatus(1) - public void key_ioExceptionCausesExit1() throws IOException, SOPGPException.BadData { - when(extractCert.key((InputStream) any())).thenReturn(new Ready() { - @Override - public void writeTo(OutputStream outputStream) throws IOException { - throw new IOException(); - } - }); - SopCLI.main(new String[] {"extract-cert"}); - } - - @Test - @ExpectSystemExitWithStatus(41) - public void key_badDataCausesExit41() throws IOException, SOPGPException.BadData { - when(extractCert.key((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); - SopCLI.main(new String[] {"extract-cert"}); - } -} diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/GenerateKeyCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/GenerateKeyCmdTest.java deleted file mode 100644 index 643cf363..00000000 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/GenerateKeyCmdTest.java +++ /dev/null @@ -1,98 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.io.IOException; -import java.io.OutputStream; - -import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.InOrder; -import org.mockito.Mockito; -import sop.Ready; -import sop.SOP; -import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; -import sop.operation.GenerateKey; - -public class GenerateKeyCmdTest { - - GenerateKey generateKey; - - @BeforeEach - public void mockComponents() throws SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.MissingArg, IOException { - generateKey = mock(GenerateKey.class); - when(generateKey.generate()).thenReturn(new Ready() { - @Override - public void writeTo(OutputStream outputStream) { - - } - }); - - SOP sop = mock(SOP.class); - when(sop.generateKey()).thenReturn(generateKey); - - SopCLI.setSopInstance(sop); - } - - @Test - public void noArmor_notCalledByDefault() { - SopCLI.main(new String[] {"generate-key", "Alice"}); - verify(generateKey, never()).noArmor(); - } - - @Test - public void noArmor_passedDown() { - SopCLI.main(new String[] {"generate-key", "--no-armor", "Alice"}); - verify(generateKey, times(1)).noArmor(); - } - - @Test - public void userId_multipleUserIdsPassedDownInProperOrder() { - SopCLI.main(new String[] {"generate-key", "Alice ", "Bob "}); - - InOrder inOrder = Mockito.inOrder(generateKey); - inOrder.verify(generateKey).userId("Alice "); - inOrder.verify(generateKey).userId("Bob "); - - verify(generateKey, times(2)).userId(any()); - } - - @Test - @ExpectSystemExitWithStatus(19) - public void missingArgumentCausesExit19() throws SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.MissingArg, IOException { - // TODO: RFC4880-bis and the current Stateless OpenPGP CLI spec allow keys to have no user-ids, - // so we might want to change this test in the future. - when(generateKey.generate()).thenThrow(new SOPGPException.MissingArg("Missing user-id.")); - SopCLI.main(new String[] {"generate-key"}); - } - - @Test - @ExpectSystemExitWithStatus(13) - public void unsupportedAsymmetricAlgorithmCausesExit13() throws SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.MissingArg, IOException { - when(generateKey.generate()).thenThrow(new SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", new Exception())); - SopCLI.main(new String[] {"generate-key", "Alice"}); - } - - @Test - @ExpectSystemExitWithStatus(1) - public void ioExceptionCausesExit1() throws SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.MissingArg, IOException { - when(generateKey.generate()).thenReturn(new Ready() { - @Override - public void writeTo(OutputStream outputStream) throws IOException { - throw new IOException(); - } - }); - SopCLI.main(new String[] {"generate-key", "Alice"}); - } -} diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/SignCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/SignCmdTest.java deleted file mode 100644 index 8de61409..00000000 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/SignCmdTest.java +++ /dev/null @@ -1,127 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import sop.Ready; -import sop.SOP; -import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; -import sop.operation.Sign; - -public class SignCmdTest { - - Sign sign; - File keyFile; - - @BeforeEach - public void mockComponents() throws IOException, SOPGPException.ExpectedText { - sign = mock(Sign.class); - when(sign.data((InputStream) any())).thenReturn(new Ready() { - @Override - public void writeTo(OutputStream outputStream) { - - } - }); - - SOP sop = mock(SOP.class); - when(sop.sign()).thenReturn(sign); - - SopCLI.setSopInstance(sop); - - keyFile = File.createTempFile("sign-", ".asc"); - } - - @Test - public void as_optionsAreCaseInsensitive() { - SopCLI.main(new String[] {"sign", "--as", "Binary", keyFile.getAbsolutePath()}); - SopCLI.main(new String[] {"sign", "--as", "binary", keyFile.getAbsolutePath()}); - SopCLI.main(new String[] {"sign", "--as", "BINARY", keyFile.getAbsolutePath()}); - } - - @Test - @ExpectSystemExitWithStatus(37) - public void as_invalidOptionCausesExit37() { - SopCLI.main(new String[] {"sign", "--as", "Invalid", keyFile.getAbsolutePath()}); - } - - @Test - @ExpectSystemExitWithStatus(37) - public void as_unsupportedOptionCausesExit37() throws SOPGPException.UnsupportedOption { - when(sign.mode(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting signing mode not supported.")); - SopCLI.main(new String[] {"sign", "--as", "binary", keyFile.getAbsolutePath()}); - } - - @Test - @ExpectSystemExitWithStatus(1) - public void key_nonExistentKeyFileCausesExit1() { - SopCLI.main(new String[] {"sign", "invalid.asc"}); - } - - @Test - @ExpectSystemExitWithStatus(1) - public void key_keyIsProtectedCausesExit1() throws SOPGPException.KeyIsProtected, IOException, SOPGPException.BadData { - when(sign.key((InputStream) any())).thenThrow(new SOPGPException.KeyIsProtected()); - SopCLI.main(new String[] {"sign", keyFile.getAbsolutePath()}); - } - - @Test - @ExpectSystemExitWithStatus(41) - public void key_badDataCausesExit41() throws SOPGPException.KeyIsProtected, IOException, SOPGPException.BadData { - when(sign.key((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); - SopCLI.main(new String[] {"sign", keyFile.getAbsolutePath()}); - } - - @Test - @ExpectSystemExitWithStatus(19) - public void key_missingKeyFileCausesExit19() { - SopCLI.main(new String[] {"sign"}); - } - - @Test - public void noArmor_notCalledByDefault() { - SopCLI.main(new String[] {"sign", keyFile.getAbsolutePath()}); - verify(sign, never()).noArmor(); - } - - @Test - public void noArmor_passedDown() { - SopCLI.main(new String[] {"sign", "--no-armor", keyFile.getAbsolutePath()}); - verify(sign, times(1)).noArmor(); - } - - @Test - @ExpectSystemExitWithStatus(1) - public void data_ioExceptionCausesExit1() throws IOException, SOPGPException.ExpectedText { - when(sign.data((InputStream) any())).thenReturn(new Ready() { - @Override - public void writeTo(OutputStream outputStream) throws IOException { - throw new IOException(); - } - }); - SopCLI.main(new String[] {"sign", keyFile.getAbsolutePath()}); - } - - @Test - @ExpectSystemExitWithStatus(53) - public void data_expectedTextExceptionCausesExit53() throws IOException, SOPGPException.ExpectedText { - when(sign.data((InputStream) any())).thenThrow(new SOPGPException.ExpectedText()); - SopCLI.main(new String[] {"sign", keyFile.getAbsolutePath()}); - } -} diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/VerifyCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/VerifyCmdTest.java deleted file mode 100644 index 028d2451..00000000 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/VerifyCmdTest.java +++ /dev/null @@ -1,204 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.PrintStream; -import java.util.Arrays; -import java.util.Collections; -import java.util.Date; - -import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentMatchers; -import sop.SOP; -import sop.Verification; -import sop.cli.picocli.DateParser; -import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; -import sop.operation.Verify; -import sop.util.UTCUtil; - -public class VerifyCmdTest { - - Verify verify; - File signature; - File cert; - - PrintStream originalSout; - - @BeforeEach - public void prepare() throws SOPGPException.UnsupportedOption, SOPGPException.BadData, SOPGPException.NoSignature, IOException { - originalSout = System.out; - - verify = mock(Verify.class); - when(verify.notBefore(any())).thenReturn(verify); - when(verify.notAfter(any())).thenReturn(verify); - when(verify.cert((InputStream) any())).thenReturn(verify); - when(verify.signatures((InputStream) any())).thenReturn(verify); - when(verify.data((InputStream) any())).thenReturn( - Collections.singletonList( - new Verification( - UTCUtil.parseUTCDate("2019-10-29T18:36:45Z"), - "EB85BB5FA33A75E15E944E63F231550C4F47E38E", - "EB85BB5FA33A75E15E944E63F231550C4F47E38E") - ) - ); - - SOP sop = mock(SOP.class); - when(sop.verify()).thenReturn(verify); - - SopCLI.setSopInstance(sop); - - signature = File.createTempFile("signature-", ".asc"); - cert = File.createTempFile("cert-", ".asc"); - } - - @AfterEach - public void restoreSout() { - System.setOut(originalSout); - } - - @Test - public void notAfter_passedDown() throws SOPGPException.UnsupportedOption { - Date date = UTCUtil.parseUTCDate("2019-10-29T18:36:45Z"); - SopCLI.main(new String[] {"verify", "--not-after", "2019-10-29T18:36:45Z", signature.getAbsolutePath(), cert.getAbsolutePath()}); - verify(verify, times(1)).notAfter(date); - } - - @Test - public void notAfter_now() throws SOPGPException.UnsupportedOption { - Date now = new Date(); - SopCLI.main(new String[] {"verify", "--not-after", "now", signature.getAbsolutePath(), cert.getAbsolutePath()}); - verify(verify, times(1)).notAfter(dateMatcher(now)); - } - - @Test - public void notAfter_dashCountsAsEndOfTime() throws SOPGPException.UnsupportedOption { - SopCLI.main(new String[] {"verify", "--not-after", "-", signature.getAbsolutePath(), cert.getAbsolutePath()}); - verify(verify, times(1)).notAfter(DateParser.END_OF_TIME); - } - - @Test - @ExpectSystemExitWithStatus(37) - public void notAfter_unsupportedOptionCausesExit37() throws SOPGPException.UnsupportedOption { - when(verify.notAfter(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting upper signature date boundary not supported.")); - SopCLI.main(new String[] {"verify", "--not-after", "2019-10-29T18:36:45Z", signature.getAbsolutePath(), cert.getAbsolutePath()}); - } - - @Test - public void notBefore_passedDown() throws SOPGPException.UnsupportedOption { - Date date = UTCUtil.parseUTCDate("2019-10-29T18:36:45Z"); - SopCLI.main(new String[] {"verify", "--not-before", "2019-10-29T18:36:45Z", signature.getAbsolutePath(), cert.getAbsolutePath()}); - verify(verify, times(1)).notBefore(date); - } - - @Test - public void notBefore_now() throws SOPGPException.UnsupportedOption { - Date now = new Date(); - SopCLI.main(new String[] {"verify", "--not-before", "now", signature.getAbsolutePath(), cert.getAbsolutePath()}); - verify(verify, times(1)).notBefore(dateMatcher(now)); - } - - @Test - public void notBefore_dashCountsAsBeginningOfTime() throws SOPGPException.UnsupportedOption { - SopCLI.main(new String[] {"verify", "--not-before", "-", signature.getAbsolutePath(), cert.getAbsolutePath()}); - verify(verify, times(1)).notBefore(DateParser.BEGINNING_OF_TIME); - } - - @Test - @ExpectSystemExitWithStatus(37) - public void notBefore_unsupportedOptionCausesExit37() throws SOPGPException.UnsupportedOption { - when(verify.notBefore(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting lower signature date boundary not supported.")); - SopCLI.main(new String[] {"verify", "--not-before", "2019-10-29T18:36:45Z", signature.getAbsolutePath(), cert.getAbsolutePath()}); - } - - @Test - public void notBeforeAndNotAfterAreCalledWithDefaultValues() throws SOPGPException.UnsupportedOption { - SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), cert.getAbsolutePath()}); - verify(verify, times(1)).notAfter(dateMatcher(new Date())); - verify(verify, times(1)).notBefore(DateParser.BEGINNING_OF_TIME); - } - - private static Date dateMatcher(Date date) { - return ArgumentMatchers.argThat(argument -> Math.abs(argument.getTime() - date.getTime()) < 1000); - } - - @Test - @ExpectSystemExitWithStatus(1) - public void cert_fileNotFoundCausesExit1() { - SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), "invalid.asc"}); - } - - @Test - @ExpectSystemExitWithStatus(41) - public void cert_badDataCausesExit41() throws SOPGPException.BadData { - when(verify.cert((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); - SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), cert.getAbsolutePath()}); - } - - @Test - @ExpectSystemExitWithStatus(1) - public void signature_fileNotFoundCausesExit1() { - SopCLI.main(new String[] {"verify", "invalid.sig", cert.getAbsolutePath()}); - } - - @Test - @ExpectSystemExitWithStatus(41) - public void signature_badDataCausesExit41() throws SOPGPException.BadData { - when(verify.signatures((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); - SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), cert.getAbsolutePath()}); - } - - @Test - @ExpectSystemExitWithStatus(3) - public void data_noSignaturesCausesExit3() throws SOPGPException.NoSignature, IOException, SOPGPException.BadData { - when(verify.data((InputStream) any())).thenThrow(new SOPGPException.NoSignature()); - SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), cert.getAbsolutePath()}); - } - - @Test - @ExpectSystemExitWithStatus(41) - public void data_badDataCausesExit41() throws SOPGPException.NoSignature, IOException, SOPGPException.BadData { - when(verify.data((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); - SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), cert.getAbsolutePath()}); - } - - @Test - public void resultIsPrintedProperly() throws SOPGPException.NoSignature, IOException, SOPGPException.BadData { - when(verify.data((InputStream) any())).thenReturn(Arrays.asList( - new Verification(UTCUtil.parseUTCDate("2019-10-29T18:36:45Z"), - "EB85BB5FA33A75E15E944E63F231550C4F47E38E", - "EB85BB5FA33A75E15E944E63F231550C4F47E38E"), - new Verification(UTCUtil.parseUTCDate("2019-10-24T23:48:29Z"), - "C90E6D36200A1B922A1509E77618196529AE5FF8", - "C4BC2DDB38CCE96485EBE9C2F20691179038E5C6") - )); - - ByteArrayOutputStream out = new ByteArrayOutputStream(); - System.setOut(new PrintStream(out)); - - SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), cert.getAbsolutePath()}); - - System.setOut(originalSout); - - String expected = "2019-10-29T18:36:45Z EB85BB5FA33A75E15E944E63F231550C4F47E38E EB85BB5FA33A75E15E944E63F231550C4F47E38E\n" + - "2019-10-24T23:48:29Z C90E6D36200A1B922A1509E77618196529AE5FF8 C4BC2DDB38CCE96485EBE9C2F20691179038E5C6\n"; - - assertEquals(expected, out.toString()); - } -} diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/VersionCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/VersionCmdTest.java deleted file mode 100644 index 6a4d628b..00000000 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/VersionCmdTest.java +++ /dev/null @@ -1,47 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import sop.SOP; -import sop.cli.picocli.SopCLI; -import sop.operation.Version; - -public class VersionCmdTest { - - private SOP sop; - private Version version; - - @BeforeEach - public void mockComponents() { - sop = mock(SOP.class); - version = mock(Version.class); - when(version.getName()).thenReturn("MockSop"); - when(version.getVersion()).thenReturn("1.0"); - when(sop.version()).thenReturn(version); - - SopCLI.setSopInstance(sop); - } - - @Test - public void assertVersionCommandWorks() { - SopCLI.main(new String[] {"version"}); - verify(version, times(1)).getVersion(); - verify(version, times(1)).getName(); - } - - @Test - @ExpectSystemExitWithStatus(37) - public void assertInvalidOptionResultsInExit37() { - SopCLI.main(new String[] {"version", "--invalid"}); - } -} diff --git a/sop-java/README.md b/sop-java/README.md deleted file mode 100644 index 86d02008..00000000 --- a/sop-java/README.md +++ /dev/null @@ -1,33 +0,0 @@ - - -# SOP-Java - -Stateless OpenPGP Protocol for Java. - -This module contains interfaces that model the API described by the -[Stateless OpenPGP Command Line Interface](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/) specification. - -This module is not a command line application! For that, see `sop-java-picocli`. - -## Why should I use this? - -If you need to use OpenPGP functionality like encrypting/decrypting messages, or creating/verifying -signatures inside your application, you probably don't want to start from scratch and instead reuse some library. - -Instead of locking yourselves in by depending hard on that one library, you can simply depend on the interfaces from -`sop-java` and plug in a library (such as `pgpainless-sop`) that implements said interfaces. - -That way you don't make yourself dependent from a single OpenPGP library and stay flexible. -Should another library emerge, that better suits your needs (and implements `sop-java`), you can easily switch -by swapping out the dependency with minimal changes to your code. - -## Why should I *implement* this? - -Did you create an [OpenPGP](https://datatracker.ietf.org/doc/html/rfc4880) implementation that can be used in the Java ecosystem? -By implementing the `sop-java` interface, you can turn your library into a command line interface (see `sop-java-picocli`). -This allows you to plug your library into the [OpenPGP interoperability test suite](https://tests.sequoia-pgp.org/) -of the [Sequoia-PGP](https://sequoia-pgp.org/) project. \ No newline at end of file diff --git a/sop-java/build.gradle b/sop-java/build.gradle deleted file mode 100644 index c2e2f1fb..00000000 --- a/sop-java/build.gradle +++ /dev/null @@ -1,22 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -plugins { - id 'java' -} - -group 'org.pgpainless' - -repositories { - mavenCentral() -} - -dependencies { - testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion" - testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion" -} - -test { - useJUnitPlatform() -} \ No newline at end of file diff --git a/sop-java/src/main/java/sop/ByteArrayAndResult.java b/sop-java/src/main/java/sop/ByteArrayAndResult.java deleted file mode 100644 index fd2b39a7..00000000 --- a/sop-java/src/main/java/sop/ByteArrayAndResult.java +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop; - -import java.io.ByteArrayInputStream; -import java.io.InputStream; - -/** - * Tuple of a byte array and associated result object. - * @param type of result - */ -public class ByteArrayAndResult { - - private final byte[] bytes; - private final T result; - - public ByteArrayAndResult(byte[] bytes, T result) { - this.bytes = bytes; - this.result = result; - } - - /** - * Return the byte array part. - * - * @return bytes - */ - public byte[] getBytes() { - return bytes; - } - - /** - * Return the result part. - * - * @return result - */ - public T getResult() { - return result; - } - - /** - * Return the byte array part as an {@link InputStream}. - * - * @return input stream - */ - public InputStream getInputStream() { - return new ByteArrayInputStream(getBytes()); - } -} diff --git a/sop-java/src/main/java/sop/DecryptionResult.java b/sop-java/src/main/java/sop/DecryptionResult.java deleted file mode 100644 index 4f0e1ab2..00000000 --- a/sop-java/src/main/java/sop/DecryptionResult.java +++ /dev/null @@ -1,29 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import sop.util.Optional; - -public class DecryptionResult { - - private final Optional sessionKey; - private final List verifications; - - public DecryptionResult(SessionKey sessionKey, List verifications) { - this.sessionKey = Optional.ofNullable(sessionKey); - this.verifications = Collections.unmodifiableList(verifications); - } - - public Optional getSessionKey() { - return sessionKey; - } - - public List getVerifications() { - return new ArrayList<>(verifications); - } -} diff --git a/sop-java/src/main/java/sop/Ready.java b/sop-java/src/main/java/sop/Ready.java deleted file mode 100644 index 71ab26ec..00000000 --- a/sop-java/src/main/java/sop/Ready.java +++ /dev/null @@ -1,45 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -public abstract class Ready { - - /** - * Write the data to the provided output stream. - * - * @param outputStream output stream - * @throws IOException in case of an IO error - */ - public abstract void writeTo(OutputStream outputStream) throws IOException; - - /** - * Return the data as a byte array by writing it to a {@link ByteArrayOutputStream} first and then returning - * the array. - * - * @return data as byte array - * @throws IOException in case of an IO error - */ - public byte[] getBytes() throws IOException { - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - writeTo(bytes); - return bytes.toByteArray(); - } - - /** - * Return an input stream containing the data. - * - * @return input stream - * @throws IOException in case of an IO error - */ - public InputStream getInputStream() throws IOException { - return new ByteArrayInputStream(getBytes()); - } -} diff --git a/sop-java/src/main/java/sop/ReadyWithResult.java b/sop-java/src/main/java/sop/ReadyWithResult.java deleted file mode 100644 index 753d41d1..00000000 --- a/sop-java/src/main/java/sop/ReadyWithResult.java +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.OutputStream; - -import sop.exception.SOPGPException; - -public abstract class ReadyWithResult { - - /** - * Write the data eg. decrypted plaintext to the provided output stream and return the result of the - * processing operation. - * - * @param outputStream output stream - * @return result, eg. signatures - * - * @throws IOException in case of an IO error - * @throws SOPGPException.NoSignature if there are no valid signatures found - */ - public abstract T writeTo(OutputStream outputStream) throws IOException, SOPGPException.NoSignature; - - /** - * Return the data as a {@link ByteArrayAndResult}. - * Calling {@link ByteArrayAndResult#getBytes()} will give you access to the data as byte array, while - * {@link ByteArrayAndResult#getResult()} will grant access to the appended result. - * - * @return byte array and result - * @throws IOException in case of an IO error - * @throws SOPGPException.NoSignature if there are no valid signatures found - */ - public ByteArrayAndResult toByteArrayAndResult() throws IOException, SOPGPException.NoSignature { - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - T result = writeTo(bytes); - return new ByteArrayAndResult<>(bytes.toByteArray(), result); - } -} diff --git a/sop-java/src/main/java/sop/SOP.java b/sop-java/src/main/java/sop/SOP.java deleted file mode 100644 index 2c2ccf16..00000000 --- a/sop-java/src/main/java/sop/SOP.java +++ /dev/null @@ -1,95 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop; - -import sop.operation.Armor; -import sop.operation.Dearmor; -import sop.operation.Decrypt; -import sop.operation.DetachInbandSignatureAndMessage; -import sop.operation.Encrypt; -import sop.operation.ExtractCert; -import sop.operation.GenerateKey; -import sop.operation.Sign; -import sop.operation.Verify; -import sop.operation.Version; - -/** - * Stateless OpenPGP Interface. - */ -public interface SOP { - - /** - * Get information about the implementations name and version. - * - * @return version - */ - Version version(); - - /** - * Generate a secret key. - * Customize the operation using the builder {@link GenerateKey}. - * - * @return builder instance - */ - GenerateKey generateKey(); - - /** - * Extract a certificate (public key) from a secret key. - * Customize the operation using the builder {@link ExtractCert}. - * - * @return builder instance - */ - ExtractCert extractCert(); - - /** - * Create detached signatures. - * Customize the operation using the builder {@link Sign}. - * - * @return builder instance - */ - Sign sign(); - - /** - * Verify detached signatures. - * Customize the operation using the builder {@link Verify}. - * - * @return builder instance - */ - Verify verify(); - - /** - * Encrypt a message. - * Customize the operation using the builder {@link Encrypt}. - * - * @return builder instance - */ - Encrypt encrypt(); - - /** - * Decrypt a message. - * Customize the operation using the builder {@link Decrypt}. - * - * @return builder instance - */ - Decrypt decrypt(); - - /** - * Convert binary OpenPGP data to ASCII. - * Customize the operation using the builder {@link Armor}. - * - * @return builder instance - */ - Armor armor(); - - /** - * Converts ASCII armored OpenPGP data to binary. - * Customize the operation using the builder {@link Dearmor}. - * - * @return builder instance - */ - Dearmor dearmor(); - - DetachInbandSignatureAndMessage detachInbandSignatureAndMessage(); -} diff --git a/sop-java/src/main/java/sop/SessionKey.java b/sop-java/src/main/java/sop/SessionKey.java deleted file mode 100644 index 2cb054d0..00000000 --- a/sop-java/src/main/java/sop/SessionKey.java +++ /dev/null @@ -1,64 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop; - -import java.util.Arrays; - -import sop.util.HexUtil; - -public class SessionKey { - - private final byte algorithm; - private final byte[] sessionKey; - - public SessionKey(byte algorithm, byte[] sessionKey) { - this.algorithm = algorithm; - this.sessionKey = sessionKey; - } - - /** - * Return the symmetric algorithm octet. - * - * @return algorithm id - */ - public byte getAlgorithm() { - return algorithm; - } - - /** - * Return the session key. - * - * @return session key - */ - public byte[] getKey() { - return sessionKey; - } - - @Override - public int hashCode() { - return getAlgorithm() * 17 + Arrays.hashCode(getKey()); - } - - @Override - public boolean equals(Object other) { - if (other == null) { - return false; - } - if (this == other) { - return true; - } - if (!(other instanceof SessionKey)) { - return false; - } - - SessionKey otherKey = (SessionKey) other; - return getAlgorithm() == otherKey.getAlgorithm() && Arrays.equals(getKey(), otherKey.getKey()); - } - - @Override - public String toString() { - return "" + (int) getAlgorithm() + ':' + HexUtil.bytesToHex(sessionKey); - } -} diff --git a/sop-java/src/main/java/sop/Signatures.java b/sop-java/src/main/java/sop/Signatures.java deleted file mode 100644 index dd3f000d..00000000 --- a/sop-java/src/main/java/sop/Signatures.java +++ /dev/null @@ -1,21 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop; - -import java.io.IOException; -import java.io.OutputStream; - -public abstract class Signatures extends Ready { - - /** - * Write OpenPGP signatures to the provided output stream. - * - * @param signatureOutputStream output stream - * @throws IOException in case of an IO error - */ - @Override - public abstract void writeTo(OutputStream signatureOutputStream) throws IOException; - -} diff --git a/sop-java/src/main/java/sop/Verification.java b/sop-java/src/main/java/sop/Verification.java deleted file mode 100644 index e8a07555..00000000 --- a/sop-java/src/main/java/sop/Verification.java +++ /dev/null @@ -1,58 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop; - -import java.util.Date; - -import sop.util.UTCUtil; - -public class Verification { - - private final Date creationTime; - private final String signingKeyFingerprint; - private final String signingCertFingerprint; - - public Verification(Date creationTime, String signingKeyFingerprint, String signingCertFingerprint) { - this.creationTime = creationTime; - this.signingKeyFingerprint = signingKeyFingerprint; - this.signingCertFingerprint = signingCertFingerprint; - } - - /** - * Return the signatures creation time. - * - * @return signature creation time - */ - public Date getCreationTime() { - return creationTime; - } - - /** - * Return the fingerprint of the signing (sub)key. - * - * @return signing key fingerprint - */ - public String getSigningKeyFingerprint() { - return signingKeyFingerprint; - } - - /** - * Return the fingerprint fo the signing certificate. - * - * @return signing certificate fingerprint - */ - public String getSigningCertFingerprint() { - return signingCertFingerprint; - } - - @Override - public String toString() { - return UTCUtil.formatUTCDate(getCreationTime()) + - ' ' + - getSigningKeyFingerprint() + - ' ' + - getSigningCertFingerprint(); - } -} diff --git a/sop-java/src/main/java/sop/enums/ArmorLabel.java b/sop-java/src/main/java/sop/enums/ArmorLabel.java deleted file mode 100644 index aeaa6f9b..00000000 --- a/sop-java/src/main/java/sop/enums/ArmorLabel.java +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.enums; - -public enum ArmorLabel { - Auto, - Sig, - Key, - Cert, - Message -} diff --git a/sop-java/src/main/java/sop/enums/EncryptAs.java b/sop-java/src/main/java/sop/enums/EncryptAs.java deleted file mode 100644 index 2de6792b..00000000 --- a/sop-java/src/main/java/sop/enums/EncryptAs.java +++ /dev/null @@ -1,11 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.enums; - -public enum EncryptAs { - Binary, - Text, - MIME -} diff --git a/sop-java/src/main/java/sop/enums/SignAs.java b/sop-java/src/main/java/sop/enums/SignAs.java deleted file mode 100644 index fcd79f4d..00000000 --- a/sop-java/src/main/java/sop/enums/SignAs.java +++ /dev/null @@ -1,10 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.enums; - -public enum SignAs { - Binary, - Text -} diff --git a/sop-java/src/main/java/sop/enums/package-info.java b/sop-java/src/main/java/sop/enums/package-info.java deleted file mode 100644 index 67148d3e..00000000 --- a/sop-java/src/main/java/sop/enums/package-info.java +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Stateless OpenPGP Interface for Java. - * Enumerations. - */ -package sop.enums; diff --git a/sop-java/src/main/java/sop/exception/SOPGPException.java b/sop-java/src/main/java/sop/exception/SOPGPException.java deleted file mode 100644 index a8c98c9c..00000000 --- a/sop-java/src/main/java/sop/exception/SOPGPException.java +++ /dev/null @@ -1,254 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.exception; - -public abstract class SOPGPException extends RuntimeException { - - public SOPGPException() { - super(); - } - - public SOPGPException(String message) { - super(message); - } - - public SOPGPException(Throwable e) { - super(e); - } - - public SOPGPException(String message, Throwable cause) { - super(message, cause); - } - - public abstract int getExitCode(); - - public static class NoSignature extends SOPGPException { - - public static final int EXIT_CODE = 3; - - public NoSignature() { - super("No verifiable signature found."); - } - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } - - public static class UnsupportedAsymmetricAlgo extends SOPGPException { - - public static final int EXIT_CODE = 13; - - public UnsupportedAsymmetricAlgo(String message, Throwable e) { - super(message, e); - } - - public UnsupportedAsymmetricAlgo(Throwable e) { - super(e); - } - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } - - public static class CertCannotEncrypt extends SOPGPException { - public static final int EXIT_CODE = 17; - - public CertCannotEncrypt(String message, Throwable cause) { - super(message, cause); - } - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } - - public static class CertCannotSign extends Exception { - - } - - public static class MissingArg extends SOPGPException { - - public static final int EXIT_CODE = 19; - - public MissingArg(String s) { - super(s); - } - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } - - public static class IncompleteVerification extends SOPGPException { - - public static final int EXIT_CODE = 23; - - public IncompleteVerification(String message) { - super(message); - } - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } - - public static class CannotDecrypt extends SOPGPException { - - public static final int EXIT_CODE = 29; - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } - - public static class PasswordNotHumanReadable extends SOPGPException { - - public static final int EXIT_CODE = 31; - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } - - public static class UnsupportedOption extends SOPGPException { - - public static final int EXIT_CODE = 37; - - public UnsupportedOption(String message) { - super(message); - } - - public UnsupportedOption(String message, Throwable cause) { - super(message, cause); - } - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } - - public static class BadData extends SOPGPException { - - public static final int EXIT_CODE = 41; - - public BadData(Throwable e) { - super(e); - } - - public BadData(String message, BadData badData) { - super(message, badData); - } - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } - - public static class ExpectedText extends SOPGPException { - - public static final int EXIT_CODE = 53; - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } - - public static class OutputExists extends SOPGPException { - - public static final int EXIT_CODE = 59; - - public OutputExists(String message) { - super(message); - } - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } - - public static class MissingInput extends SOPGPException { - - public static final int EXIT_CODE = 61; - - public MissingInput(String message, Throwable cause) { - super(message, cause); - } - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } - - public static class KeyIsProtected extends SOPGPException { - - public static final int EXIT_CODE = 67; - - public KeyIsProtected() { - super(); - } - - public KeyIsProtected(String message, Throwable cause) { - super(message, cause); - } - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } - - public static class UnsupportedSubcommand extends SOPGPException { - - public static final int EXIT_CODE = 69; - - public UnsupportedSubcommand(String message) { - super(message); - } - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } - - public static class UnsupportedSpecialPrefix extends SOPGPException { - - public static final int EXIT_CODE = 71; - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } - - - public static class AmbiguousInput extends SOPGPException { - - public static final int EXIT_CODE = 73; - - public AmbiguousInput(String message) { - super(message); - } - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } -} diff --git a/sop-java/src/main/java/sop/exception/package-info.java b/sop-java/src/main/java/sop/exception/package-info.java deleted file mode 100644 index 4abc562b..00000000 --- a/sop-java/src/main/java/sop/exception/package-info.java +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Stateless OpenPGP Interface for Java. - * Exception classes. - */ -package sop.exception; diff --git a/sop-java/src/main/java/sop/operation/Armor.java b/sop-java/src/main/java/sop/operation/Armor.java deleted file mode 100644 index dea3257a..00000000 --- a/sop-java/src/main/java/sop/operation/Armor.java +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -import java.io.ByteArrayInputStream; -import java.io.InputStream; - -import sop.Ready; -import sop.enums.ArmorLabel; -import sop.exception.SOPGPException; - -public interface Armor { - - /** - * Overrides automatic detection of label. - * - * @param label armor label - * @return builder instance - */ - Armor label(ArmorLabel label) throws SOPGPException.UnsupportedOption; - - /** - * Armor the provided data. - * - * @param data input stream of unarmored OpenPGP data - * @return armored data - */ - Ready data(InputStream data) throws SOPGPException.BadData; - - /** - * Armor the provided data. - * - * @param data unarmored OpenPGP data - * @return armored data - */ - default Ready data(byte[] data) throws SOPGPException.BadData { - return data(new ByteArrayInputStream(data)); - } -} diff --git a/sop-java/src/main/java/sop/operation/Dearmor.java b/sop-java/src/main/java/sop/operation/Dearmor.java deleted file mode 100644 index 35eceb56..00000000 --- a/sop-java/src/main/java/sop/operation/Dearmor.java +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; - -import sop.Ready; -import sop.exception.SOPGPException; - -public interface Dearmor { - - /** - * Dearmor armored OpenPGP data. - * - * @param data armored OpenPGP data - * @return input stream of unarmored data - */ - Ready data(InputStream data) throws SOPGPException.BadData, IOException; - - /** - * Dearmor armored OpenPGP data. - * - * @param data armored OpenPGP data - * @return input stream of unarmored data - */ - default Ready data(byte[] data) throws SOPGPException.BadData, IOException { - return data(new ByteArrayInputStream(data)); - } -} diff --git a/sop-java/src/main/java/sop/operation/Decrypt.java b/sop-java/src/main/java/sop/operation/Decrypt.java deleted file mode 100644 index 4cbd6f35..00000000 --- a/sop-java/src/main/java/sop/operation/Decrypt.java +++ /dev/null @@ -1,118 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.Date; - -import sop.DecryptionResult; -import sop.ReadyWithResult; -import sop.SessionKey; -import sop.exception.SOPGPException; - -public interface Decrypt { - - /** - * Makes the SOP consider signatures before this date invalid. - * - * @param timestamp timestamp - * @return builder instance - */ - Decrypt verifyNotBefore(Date timestamp) - throws SOPGPException.UnsupportedOption; - - /** - * Makes the SOP consider signatures after this date invalid. - * - * @param timestamp timestamp - * @return builder instance - */ - Decrypt verifyNotAfter(Date timestamp) - throws SOPGPException.UnsupportedOption; - - /** - * Adds the verification cert. - * - * @param cert input stream containing the cert - * @return builder instance - */ - Decrypt verifyWithCert(InputStream cert) - throws SOPGPException.BadData, - IOException; - - /** - * Adds the verification cert. - * - * @param cert byte array containing the cert - * @return builder instance - */ - default Decrypt verifyWithCert(byte[] cert) - throws SOPGPException.BadData, IOException { - return verifyWithCert(new ByteArrayInputStream(cert)); - } - - /** - * Tries to decrypt with the given session key. - * - * @param sessionKey session key - * @return builder instance - */ - Decrypt withSessionKey(SessionKey sessionKey) - throws SOPGPException.UnsupportedOption; - - /** - * Tries to decrypt with the given password. - * - * @param password password - * @return builder instance - */ - Decrypt withPassword(String password) - throws SOPGPException.PasswordNotHumanReadable, - SOPGPException.UnsupportedOption; - - /** - * Adds the decryption key. - * - * @param key input stream containing the key - * @return builder instance - */ - Decrypt withKey(InputStream key) - throws SOPGPException.KeyIsProtected, - SOPGPException.BadData, - SOPGPException.UnsupportedAsymmetricAlgo; - - /** - * Adds the decryption key. - * - * @param key byte array containing the key - * @return builder instance - */ - default Decrypt withKey(byte[] key) - throws SOPGPException.KeyIsProtected, - SOPGPException.BadData, - SOPGPException.UnsupportedAsymmetricAlgo { - return withKey(new ByteArrayInputStream(key)); - } - - /** - * Decrypts the given ciphertext, returning verification results and plaintext. - * @param ciphertext ciphertext - * @return ready with result - */ - ReadyWithResult ciphertext(InputStream ciphertext) - throws SOPGPException.BadData, SOPGPException.MissingArg, SOPGPException.CannotDecrypt; - - /** - * Decrypts the given ciphertext, returning verification results and plaintext. - * @param ciphertext ciphertext - * @return ready with result - */ - default ReadyWithResult ciphertext(byte[] ciphertext) - throws SOPGPException.BadData, SOPGPException.MissingArg, SOPGPException.CannotDecrypt { - return ciphertext(new ByteArrayInputStream(ciphertext)); - } -} diff --git a/sop-java/src/main/java/sop/operation/DetachInbandSignatureAndMessage.java b/sop-java/src/main/java/sop/operation/DetachInbandSignatureAndMessage.java deleted file mode 100644 index 46bd3f77..00000000 --- a/sop-java/src/main/java/sop/operation/DetachInbandSignatureAndMessage.java +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; - -import sop.ReadyWithResult; -import sop.Signatures; - -public interface DetachInbandSignatureAndMessage { - - /** - * Do not wrap the signatures in ASCII armor. - * @return builder - */ - DetachInbandSignatureAndMessage noArmor(); - - /** - * Detach the provided cleartext signed message from its signatures. - * - * @param messageInputStream input stream containing the signed message - * @return result containing the detached message - * @throws IOException in case of an IO error - */ - ReadyWithResult message(InputStream messageInputStream) throws IOException; - - /** - * Detach the provided cleartext signed message from its signatures. - * - * @param message byte array containing the signed message - * @return result containing the detached message - * @throws IOException in case of an IO error - */ - default ReadyWithResult message(byte[] message) throws IOException { - return message(new ByteArrayInputStream(message)); - } -} diff --git a/sop-java/src/main/java/sop/operation/Encrypt.java b/sop-java/src/main/java/sop/operation/Encrypt.java deleted file mode 100644 index b5a92b25..00000000 --- a/sop-java/src/main/java/sop/operation/Encrypt.java +++ /dev/null @@ -1,109 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; - -import sop.Ready; -import sop.enums.EncryptAs; -import sop.exception.SOPGPException; - -public interface Encrypt { - - /** - * Disable ASCII armor encoding. - * - * @return builder instance - */ - Encrypt noArmor(); - - /** - * Sets encryption mode. - * - * @param mode mode - * @return builder instance - */ - Encrypt mode(EncryptAs mode) - throws SOPGPException.UnsupportedOption; - - /** - * Adds the signer key. - * - * @param key input stream containing the encoded signer key - * @return builder instance - */ - Encrypt signWith(InputStream key) - throws SOPGPException.KeyIsProtected, - SOPGPException.CertCannotSign, - SOPGPException.UnsupportedAsymmetricAlgo, - SOPGPException.BadData; - - /** - * Adds the signer key. - * - * @param key byte array containing the encoded signer key - * @return builder instance - */ - default Encrypt signWith(byte[] key) - throws SOPGPException.KeyIsProtected, - SOPGPException.CertCannotSign, - SOPGPException.UnsupportedAsymmetricAlgo, - SOPGPException.BadData { - return signWith(new ByteArrayInputStream(key)); - } - - /** - * Encrypt with the given password. - * - * @param password password - * @return builder instance - */ - Encrypt withPassword(String password) - throws SOPGPException.PasswordNotHumanReadable, - SOPGPException.UnsupportedOption; - - /** - * Encrypt with the given cert. - * - * @param cert input stream containing the encoded cert. - * @return builder instance - */ - Encrypt withCert(InputStream cert) - throws SOPGPException.CertCannotEncrypt, - SOPGPException.UnsupportedAsymmetricAlgo, - SOPGPException.BadData; - - /** - * Encrypt with the given cert. - * - * @param cert byte array containing the encoded cert. - * @return builder instance - */ - default Encrypt withCert(byte[] cert) - throws SOPGPException.CertCannotEncrypt, - SOPGPException.UnsupportedAsymmetricAlgo, - SOPGPException.BadData { - return withCert(new ByteArrayInputStream(cert)); - } - - /** - * Encrypt the given data yielding the ciphertext. - * @param plaintext plaintext - * @return input stream containing the ciphertext - */ - Ready plaintext(InputStream plaintext) - throws IOException; - - /** - * Encrypt the given data yielding the ciphertext. - * @param plaintext plaintext - * @return input stream containing the ciphertext - */ - default Ready plaintext(byte[] plaintext) throws IOException { - return plaintext(new ByteArrayInputStream(plaintext)); - } -} diff --git a/sop-java/src/main/java/sop/operation/ExtractCert.java b/sop-java/src/main/java/sop/operation/ExtractCert.java deleted file mode 100644 index 7a0de5c6..00000000 --- a/sop-java/src/main/java/sop/operation/ExtractCert.java +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; - -import sop.Ready; -import sop.exception.SOPGPException; - -public interface ExtractCert { - - /** - * Disable ASCII armor encoding. - * - * @return builder instance - */ - ExtractCert noArmor(); - - /** - * Extract the cert from the provided key. - * - * @param keyInputStream input stream containing the encoding of an OpenPGP key - * @return result containing the encoding of the keys cert - */ - Ready key(InputStream keyInputStream) throws IOException, SOPGPException.BadData; - - /** - * Extract the cert from the provided key. - * - * @param key byte array containing the encoding of an OpenPGP key - * @return result containing the encoding of the keys cert - */ - default Ready key(byte[] key) throws IOException, SOPGPException.BadData { - return key(new ByteArrayInputStream(key)); - } -} diff --git a/sop-java/src/main/java/sop/operation/GenerateKey.java b/sop-java/src/main/java/sop/operation/GenerateKey.java deleted file mode 100644 index c652e84a..00000000 --- a/sop-java/src/main/java/sop/operation/GenerateKey.java +++ /dev/null @@ -1,36 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -import java.io.IOException; -import java.io.InputStream; - -import sop.Ready; -import sop.exception.SOPGPException; - -public interface GenerateKey { - - /** - * Disable ASCII armor encoding. - * - * @return builder instance - */ - GenerateKey noArmor(); - - /** - * Adds a user-id. - * - * @param userId user-id - * @return builder instance - */ - GenerateKey userId(String userId); - - /** - * Generate the OpenPGP key and return it encoded as an {@link InputStream}. - * - * @return key - */ - Ready generate() throws SOPGPException.MissingArg, SOPGPException.UnsupportedAsymmetricAlgo, IOException; -} diff --git a/sop-java/src/main/java/sop/operation/Sign.java b/sop-java/src/main/java/sop/operation/Sign.java deleted file mode 100644 index 9b9c3a6f..00000000 --- a/sop-java/src/main/java/sop/operation/Sign.java +++ /dev/null @@ -1,68 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; - -import sop.Ready; -import sop.enums.SignAs; -import sop.exception.SOPGPException; - -public interface Sign { - - /** - * Disable ASCII armor encoding. - * - * @return builder instance - */ - Sign noArmor(); - - /** - * Sets the signature mode. - * Note: This method has to be called before {@link #key(InputStream)} is called. - * - * @param mode signature mode - * @return builder instance - */ - Sign mode(SignAs mode) throws SOPGPException.UnsupportedOption; - - /** - * Adds the signer key. - * - * @param key input stream containing encoded key - * @return builder instance - */ - Sign key(InputStream key) throws SOPGPException.KeyIsProtected, SOPGPException.BadData, IOException; - - /** - * Adds the signer key. - * - * @param key byte array containing encoded key - * @return builder instance - */ - default Sign key(byte[] key) throws SOPGPException.KeyIsProtected, SOPGPException.BadData, IOException { - return key(new ByteArrayInputStream(key)); - } - - /** - * Signs data. - * - * @param data input stream containing data - * @return ready - */ - Ready data(InputStream data) throws IOException, SOPGPException.ExpectedText; - - /** - * Signs data. - * - * @param data byte array containing data - * @return ready - */ - default Ready data(byte[] data) throws IOException, SOPGPException.ExpectedText { - return data(new ByteArrayInputStream(data)); - } -} diff --git a/sop-java/src/main/java/sop/operation/Verify.java b/sop-java/src/main/java/sop/operation/Verify.java deleted file mode 100644 index 30905de2..00000000 --- a/sop-java/src/main/java/sop/operation/Verify.java +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -import java.io.ByteArrayInputStream; -import java.io.InputStream; -import java.util.Date; - -import sop.exception.SOPGPException; - -public interface Verify extends VerifySignatures { - - /** - * Makes the SOP implementation consider signatures before this date invalid. - * - * @param timestamp timestamp - * @return builder instance - */ - Verify notBefore(Date timestamp) throws SOPGPException.UnsupportedOption; - - /** - * Makes the SOP implementation consider signatures after this date invalid. - * - * @param timestamp timestamp - * @return builder instance - */ - Verify notAfter(Date timestamp) throws SOPGPException.UnsupportedOption; - - /** - * Adds the verification cert. - * - * @param cert input stream containing the encoded cert - * @return builder instance - */ - Verify cert(InputStream cert) throws SOPGPException.BadData; - - /** - * Adds the verification cert. - * - * @param cert byte array containing the encoded cert - * @return builder instance - */ - default Verify cert(byte[] cert) throws SOPGPException.BadData { - return cert(new ByteArrayInputStream(cert)); - } - - /** - * Provides the signatures. - * @param signatures input stream containing encoded, detached signatures. - * - * @return builder instance - */ - VerifySignatures signatures(InputStream signatures) throws SOPGPException.BadData; - - /** - * Provides the signatures. - * @param signatures byte array containing encoded, detached signatures. - * - * @return builder instance - */ - default VerifySignatures signatures(byte[] signatures) throws SOPGPException.BadData { - return signatures(new ByteArrayInputStream(signatures)); - } - -} diff --git a/sop-java/src/main/java/sop/operation/VerifySignatures.java b/sop-java/src/main/java/sop/operation/VerifySignatures.java deleted file mode 100644 index d41a8edd..00000000 --- a/sop-java/src/main/java/sop/operation/VerifySignatures.java +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.List; - -import sop.Verification; -import sop.exception.SOPGPException; - -public interface VerifySignatures { - - /** - * Provide the signed data (without signatures). - * - * @param data signed data - * @return list of signature verifications - * @throws IOException in case of an IO error - * @throws SOPGPException.NoSignature when no signature is found - * @throws SOPGPException.BadData when the data is invalid OpenPGP data - */ - List data(InputStream data) throws IOException, SOPGPException.NoSignature, SOPGPException.BadData; - - /** - * Provide the signed data (without signatures). - * - * @param data signed data - * @return list of signature verifications - * @throws IOException in case of an IO error - * @throws SOPGPException.NoSignature when no signature is found - * @throws SOPGPException.BadData when the data is invalid OpenPGP data - */ - default List data(byte[] data) throws IOException, SOPGPException.NoSignature, SOPGPException.BadData { - return data(new ByteArrayInputStream(data)); - } -} diff --git a/sop-java/src/main/java/sop/operation/Version.java b/sop-java/src/main/java/sop/operation/Version.java deleted file mode 100644 index ab32099a..00000000 --- a/sop-java/src/main/java/sop/operation/Version.java +++ /dev/null @@ -1,22 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -public interface Version { - - /** - * Return the implementations name. - * - * @return implementation name - */ - String getName(); - - /** - * Return the implementations version string. - * - * @return version string - */ - String getVersion(); -} diff --git a/sop-java/src/main/java/sop/operation/package-info.java b/sop-java/src/main/java/sop/operation/package-info.java deleted file mode 100644 index dde4d5bb..00000000 --- a/sop-java/src/main/java/sop/operation/package-info.java +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Stateless OpenPGP Interface for Java. - * Different cryptographic operations. - */ -package sop.operation; diff --git a/sop-java/src/main/java/sop/package-info.java b/sop-java/src/main/java/sop/package-info.java deleted file mode 100644 index 5ad4f528..00000000 --- a/sop-java/src/main/java/sop/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Stateless OpenPGP Interface for Java. - */ -package sop; diff --git a/sop-java/src/main/java/sop/util/HexUtil.java b/sop-java/src/main/java/sop/util/HexUtil.java deleted file mode 100644 index a70346e2..00000000 --- a/sop-java/src/main/java/sop/util/HexUtil.java +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright 2021 Paul Schaub, @maybeWeCouldStealAVan, @Dave L. -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.util; - -public class HexUtil { - - private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray(); - - /** - * Encode a byte array to a hex string. - * - * @see - * How to convert a byte array to a hex string in Java? - * @param bytes - * @return - */ - public static String bytesToHex(byte[] bytes) { - char[] hexChars = new char[bytes.length * 2]; - for (int j = 0; j < bytes.length; j++) { - int v = bytes[j] & 0xFF; - hexChars[j * 2] = HEX_ARRAY[v >>> 4]; - hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F]; - } - return new String(hexChars); - } - - /** - * Decode a hex string into a byte array. - * - * @see - * Convert a string representation of a hex dump to a byte array using Java? - * @param s hex string - * @return decoded byte array - */ - public static byte[] hexToBytes(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; - } -} diff --git a/sop-java/src/main/java/sop/util/Optional.java b/sop-java/src/main/java/sop/util/Optional.java deleted file mode 100644 index 00eb2012..00000000 --- a/sop-java/src/main/java/sop/util/Optional.java +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.util; - -/** - * Backport of java.util.Optional for older Android versions. - * - * @param item type - */ -public class Optional { - - private final T item; - - public Optional() { - this(null); - } - - public Optional(T item) { - this.item = item; - } - - public static Optional of(T item) { - if (item == null) { - throw new NullPointerException("Item cannot be null."); - } - return new Optional<>(item); - } - - public static Optional ofNullable(T item) { - return new Optional<>(item); - } - - public static Optional ofEmpty() { - return new Optional<>(null); - } - - public T get() { - return item; - } - - public boolean isPresent() { - return item != null; - } - - public boolean isEmpty() { - return item == null; - } -} diff --git a/sop-java/src/main/java/sop/util/ProxyOutputStream.java b/sop-java/src/main/java/sop/util/ProxyOutputStream.java deleted file mode 100644 index 516d7c92..00000000 --- a/sop-java/src/main/java/sop/util/ProxyOutputStream.java +++ /dev/null @@ -1,80 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.util; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.OutputStream; - -/** - * {@link OutputStream} that buffers data being written into it, until its underlying output stream is being replaced. - * At that point, first all the buffered data is being written to the underlying stream, followed by any successive - * data that may get written to the {@link ProxyOutputStream}. - * - * This class is useful if we need to provide an {@link OutputStream} at one point in time where the final - * target output stream is not yet known. - */ -public class ProxyOutputStream extends OutputStream { - - private final ByteArrayOutputStream buffer; - private OutputStream swapped; - - public ProxyOutputStream() { - this.buffer = new ByteArrayOutputStream(); - } - - public synchronized void replaceOutputStream(OutputStream underlying) throws IOException { - if (underlying == null) { - throw new NullPointerException("Underlying OutputStream cannot be null."); - } - this.swapped = underlying; - - byte[] bufferBytes = buffer.toByteArray(); - swapped.write(bufferBytes); - } - - @Override - public synchronized void write(byte[] b) throws IOException { - if (swapped == null) { - buffer.write(b); - } else { - swapped.write(b); - } - } - - @Override - public synchronized void write(byte[] b, int off, int len) throws IOException { - if (swapped == null) { - buffer.write(b, off, len); - } else { - swapped.write(b, off, len); - } - } - - @Override - public synchronized void flush() throws IOException { - buffer.flush(); - if (swapped != null) { - swapped.flush(); - } - } - - @Override - public synchronized void close() throws IOException { - buffer.close(); - if (swapped != null) { - swapped.close(); - } - } - - @Override - public synchronized void write(int i) throws IOException { - if (swapped == null) { - buffer.write(i); - } else { - swapped.write(i); - } - } -} diff --git a/sop-java/src/main/java/sop/util/UTCUtil.java b/sop-java/src/main/java/sop/util/UTCUtil.java deleted file mode 100644 index 646ef25b..00000000 --- a/sop-java/src/main/java/sop/util/UTCUtil.java +++ /dev/null @@ -1,55 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.util; - -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.TimeZone; - -/** - * Utility class to parse and format dates as ISO-8601 UTC timestamps. - */ -public class UTCUtil { - - public static SimpleDateFormat UTC_FORMATTER = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); - public static SimpleDateFormat[] UTC_PARSERS = new SimpleDateFormat[] { - UTC_FORMATTER, - new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX"), - new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'"), - new SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'") - }; - - static { - for (SimpleDateFormat f : UTC_PARSERS) { - f.setTimeZone(TimeZone.getTimeZone("UTC")); - } - } - /** - * Parse an ISO-8601 UTC timestamp from a string. - * - * @param dateString string - * @return date - */ - public static Date parseUTCDate(String dateString) { - for (SimpleDateFormat parser : UTC_PARSERS) { - try { - return parser.parse(dateString); - } catch (ParseException e) { - } - } - return null; - } - - /** - * Format a date as ISO-8601 UTC timestamp. - * - * @param date date - * @return timestamp string - */ - public static String formatUTCDate(Date date) { - return UTC_FORMATTER.format(date); - } -} diff --git a/sop-java/src/main/java/sop/util/package-info.java b/sop-java/src/main/java/sop/util/package-info.java deleted file mode 100644 index 3dd9fc19..00000000 --- a/sop-java/src/main/java/sop/util/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Utility classes. - */ -package sop.util; diff --git a/sop-java/src/test/java/sop/util/ByteArrayAndResultTest.java b/sop-java/src/test/java/sop/util/ByteArrayAndResultTest.java deleted file mode 100644 index 8ae1859f..00000000 --- a/sop-java/src/test/java/sop/util/ByteArrayAndResultTest.java +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.util; - -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; - -import java.nio.charset.StandardCharsets; -import java.util.Collections; -import java.util.List; - -import org.junit.jupiter.api.Test; -import sop.ByteArrayAndResult; -import sop.Verification; - -public class ByteArrayAndResultTest { - - @Test - public void testCreationAndGetters() { - byte[] bytes = "Hello, World!\n".getBytes(StandardCharsets.UTF_8); - List result = Collections.singletonList( - new Verification(UTCUtil.parseUTCDate("2019-10-24T23:48:29Z"), - "C90E6D36200A1B922A1509E77618196529AE5FF8", - "C4BC2DDB38CCE96485EBE9C2F20691179038E5C6") - ); - ByteArrayAndResult> bytesAndResult = new ByteArrayAndResult<>(bytes, result); - - assertArrayEquals(bytes, bytesAndResult.getBytes()); - assertEquals(result, bytesAndResult.getResult()); - } -} diff --git a/sop-java/src/test/java/sop/util/HexUtilTest.java b/sop-java/src/test/java/sop/util/HexUtilTest.java deleted file mode 100644 index c8f32ee9..00000000 --- a/sop-java/src/test/java/sop/util/HexUtilTest.java +++ /dev/null @@ -1,62 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.util; - -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; - -import java.nio.charset.Charset; - -import org.junit.jupiter.api.Test; - -/** - * Test using some test vectors from RFC4648. - * - * @see RFC-4648 §10: Test Vectors - */ -public class HexUtilTest { - - private static final Charset ASCII = Charset.forName("US-ASCII"); - - @Test - public void emptyHexEncodeTest() { - assertHexEquals("", ""); - } - - @Test - public void encodeF() { - assertHexEquals("66", "f"); - } - - @Test - public void encodeFo() { - assertHexEquals("666F", "fo"); - } - - @Test - public void encodeFoo() { - assertHexEquals("666F6F", "foo"); - } - - @Test - public void encodeFoob() { - assertHexEquals("666F6F62", "foob"); - } - - @Test - public void encodeFooba() { - assertHexEquals("666F6F6261", "fooba"); - } - - @Test - public void encodeFoobar() { - assertHexEquals("666F6F626172", "foobar"); - } - - private void assertHexEquals(String hex, String ascii) { - assertEquals(hex, HexUtil.bytesToHex(ascii.getBytes(ASCII))); - assertArrayEquals(ascii.getBytes(ASCII), HexUtil.hexToBytes(hex)); - } -} diff --git a/sop-java/src/test/java/sop/util/OptionalTest.java b/sop-java/src/test/java/sop/util/OptionalTest.java deleted file mode 100644 index 45900b73..00000000 --- a/sop-java/src/test/java/sop/util/OptionalTest.java +++ /dev/null @@ -1,78 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.util; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import org.junit.jupiter.api.Test; - -public class OptionalTest { - - @Test - public void testEmpty() { - Optional optional = new Optional<>(); - assertEmpty(optional); - } - - @Test - public void testArg() { - String string = "foo"; - Optional optional = new Optional<>(string); - assertFalse(optional.isEmpty()); - assertTrue(optional.isPresent()); - assertEquals(string, optional.get()); - } - - @Test - public void testOfEmpty() { - Optional optional = Optional.ofEmpty(); - assertEmpty(optional); - } - - @Test - public void testNullArg() { - Optional optional = new Optional<>(null); - assertEmpty(optional); - } - - @Test - public void testOfWithNullArgThrows() { - assertThrows(NullPointerException.class, () -> Optional.of(null)); - } - - @Test - public void testOf() { - String string = "Hello, World!"; - Optional optional = Optional.of(string); - assertFalse(optional.isEmpty()); - assertTrue(optional.isPresent()); - assertEquals(string, optional.get()); - } - - @Test - public void testOfNullableWithNull() { - Optional optional = Optional.ofNullable(null); - assertEmpty(optional); - } - - @Test - public void testOfNullableWithArg() { - Optional optional = Optional.ofNullable("bar"); - assertEquals("bar", optional.get()); - assertFalse(optional.isEmpty()); - assertTrue(optional.isPresent()); - } - - private void assertEmpty(Optional optional) { - assertTrue(optional.isEmpty()); - assertFalse(optional.isPresent()); - - assertNull(optional.get()); - } -} diff --git a/sop-java/src/test/java/sop/util/ProxyOutputStreamTest.java b/sop-java/src/test/java/sop/util/ProxyOutputStreamTest.java deleted file mode 100644 index 9d99fd4f..00000000 --- a/sop-java/src/test/java/sop/util/ProxyOutputStreamTest.java +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.util; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.nio.charset.StandardCharsets; - -import org.junit.jupiter.api.Test; - -public class ProxyOutputStreamTest { - - @Test - public void replaceOutputStreamThrowsNPEForNull() { - ProxyOutputStream proxy = new ProxyOutputStream(); - assertThrows(NullPointerException.class, () -> proxy.replaceOutputStream(null)); - } - - @Test - public void testSwappingStreamPreservesWrittenBytes() throws IOException { - byte[] firstSection = "Foo\nBar\n".getBytes(StandardCharsets.UTF_8); - byte[] secondSection = "Baz\n".getBytes(StandardCharsets.UTF_8); - - ProxyOutputStream proxy = new ProxyOutputStream(); - proxy.write(firstSection); - - ByteArrayOutputStream swappedStream = new ByteArrayOutputStream(); - proxy.replaceOutputStream(swappedStream); - - proxy.write(secondSection); - proxy.close(); - - assertEquals("Foo\nBar\nBaz\n", swappedStream.toString()); - } -} diff --git a/sop-java/src/test/java/sop/util/ReadyTest.java b/sop-java/src/test/java/sop/util/ReadyTest.java deleted file mode 100644 index 07fa0903..00000000 --- a/sop-java/src/test/java/sop/util/ReadyTest.java +++ /dev/null @@ -1,30 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.util; - -import static org.junit.jupiter.api.Assertions.assertArrayEquals; - -import java.io.IOException; -import java.io.OutputStream; -import java.nio.charset.StandardCharsets; - -import org.junit.jupiter.api.Test; -import sop.Ready; - -public class ReadyTest { - - @Test - public void readyTest() throws IOException { - byte[] data = "Hello, World!\n".getBytes(StandardCharsets.UTF_8); - Ready ready = new Ready() { - @Override - public void writeTo(OutputStream outputStream) throws IOException { - outputStream.write(data); - } - }; - - assertArrayEquals(data, ready.getBytes()); - } -} diff --git a/sop-java/src/test/java/sop/util/ReadyWithResultTest.java b/sop-java/src/test/java/sop/util/ReadyWithResultTest.java deleted file mode 100644 index 97841fa8..00000000 --- a/sop-java/src/test/java/sop/util/ReadyWithResultTest.java +++ /dev/null @@ -1,44 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.util; - -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; - -import java.io.IOException; -import java.io.OutputStream; -import java.nio.charset.StandardCharsets; -import java.util.Collections; -import java.util.List; - -import org.junit.jupiter.api.Test; -import sop.ByteArrayAndResult; -import sop.ReadyWithResult; -import sop.Verification; -import sop.exception.SOPGPException; - -public class ReadyWithResultTest { - - @Test - public void testReadyWithResult() throws SOPGPException.NoSignature, IOException { - byte[] data = "Hello, World!\n".getBytes(StandardCharsets.UTF_8); - List result = Collections.singletonList( - new Verification(UTCUtil.parseUTCDate("2019-10-24T23:48:29Z"), - "C90E6D36200A1B922A1509E77618196529AE5FF8", - "C4BC2DDB38CCE96485EBE9C2F20691179038E5C6") - ); - ReadyWithResult> readyWithResult = new ReadyWithResult>() { - @Override - public List writeTo(OutputStream outputStream) throws IOException, SOPGPException.NoSignature { - outputStream.write(data); - return result; - } - }; - - ByteArrayAndResult> bytesAndResult = readyWithResult.toByteArrayAndResult(); - assertArrayEquals(data, bytesAndResult.getBytes()); - assertEquals(result, bytesAndResult.getResult()); - } -} diff --git a/sop-java/src/test/java/sop/util/SessionKeyTest.java b/sop-java/src/test/java/sop/util/SessionKeyTest.java deleted file mode 100644 index b79fd81b..00000000 --- a/sop-java/src/test/java/sop/util/SessionKeyTest.java +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.util; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotEquals; - -import org.junit.jupiter.api.Test; -import sop.SessionKey; - -public class SessionKeyTest { - - @Test - public void toStringTest() { - SessionKey sessionKey = new SessionKey((byte) 9, HexUtil.hexToBytes("FCA4BEAF687F48059CACC14FB019125CD57392BAB7037C707835925CBF9F7BCD")); - assertEquals("9:FCA4BEAF687F48059CACC14FB019125CD57392BAB7037C707835925CBF9F7BCD", sessionKey.toString()); - } - - @Test - public void equalsTest() { - SessionKey s1 = new SessionKey((byte) 9, HexUtil.hexToBytes("FCA4BEAF687F48059CACC14FB019125CD57392BAB7037C707835925CBF9F7BCD")); - SessionKey s2 = new SessionKey((byte) 9, HexUtil.hexToBytes("FCA4BEAF687F48059CACC14FB019125CD57392BAB7037C707835925CBF9F7BCD")); - SessionKey s3 = new SessionKey((byte) 4, HexUtil.hexToBytes("FCA4BEAF687F48059CACC14FB019125CD57392BAB7037C707835925CBF9F7BCD")); - SessionKey s4 = new SessionKey((byte) 9, HexUtil.hexToBytes("19125CD57392BAB7037C7078359FCA4BEAF687F4025CBF9F7BCD8059CACC14FB")); - SessionKey s5 = new SessionKey((byte) 4, HexUtil.hexToBytes("19125CD57392BAB7037C7078359FCA4BEAF687F4025CBF9F7BCD8059CACC14FB")); - - assertEquals(s1, s1); - assertEquals(s1, s2); - assertEquals(s1.hashCode(), s2.hashCode()); - assertNotEquals(s1, s3); - assertNotEquals(s1.hashCode(), s3.hashCode()); - assertNotEquals(s1, s4); - assertNotEquals(s1.hashCode(), s4.hashCode()); - assertNotEquals(s4, s5); - assertNotEquals(s4.hashCode(), s5.hashCode()); - assertNotEquals(s1, null); - assertNotEquals(s1, "FCA4BEAF687F48059CACC14FB019125CD57392BAB7037C707835925CBF9F7BCD"); - } -} diff --git a/sop-java/src/test/java/sop/util/UTCUtilTest.java b/sop-java/src/test/java/sop/util/UTCUtilTest.java deleted file mode 100644 index 18de8176..00000000 --- a/sop-java/src/test/java/sop/util/UTCUtilTest.java +++ /dev/null @@ -1,48 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.util; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; - -import java.util.Date; - -import org.junit.jupiter.api.Test; - -/** - * Test parsing some date examples from the stateless OpenPGP CLI spec. - * - * @see OpenPGP Stateless CLI §4.1. Date - */ -public class UTCUtilTest { - - @Test - public void parseExample1() { - String timestamp = "2019-10-29T12:11:04+00:00"; - Date date = UTCUtil.parseUTCDate(timestamp); - assertEquals("2019-10-29T12:11:04Z", UTCUtil.formatUTCDate(date)); - } - - @Test - public void parseExample2() { - String timestamp = "2019-10-24T23:48:29Z"; - Date date = UTCUtil.parseUTCDate(timestamp); - assertEquals("2019-10-24T23:48:29Z", UTCUtil.formatUTCDate(date)); - } - - @Test - public void parseExample3() { - String timestamp = "20191029T121104Z"; - Date date = UTCUtil.parseUTCDate(timestamp); - assertEquals("2019-10-29T12:11:04Z", UTCUtil.formatUTCDate(date)); - } - - @Test - public void invalidDateReturnsNull() { - String invalidTimestamp = "foobar"; - Date expectNull = UTCUtil.parseUTCDate(invalidTimestamp); - assertNull(expectNull); - } -} diff --git a/version.gradle b/version.gradle index b6f0de75..bc2515a4 100644 --- a/version.gradle +++ b/version.gradle @@ -4,10 +4,15 @@ allprojects { ext { - shortVersion = '0.2.17' - isSnapshot = false - pgpainlessMinAndroidSdk = 10 - javaSourceCompatibility = 1.8 - bouncyCastleVersion = '1.69' + shortVersion = '1.7.7' + isSnapshot = true + javaSourceCompatibility = 11 + bouncyCastleVersion = '1.81' + bouncyPgVersion = bouncyCastleVersion + junitVersion = '5.8.2' + logbackVersion = '1.5.13' + mockitoVersion = '4.5.1' + slf4jVersion = '1.7.36' + sopJavaVersion = '10.1.1' } }