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/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 index ab7b9e6e..d45b16a3 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -17,10 +17,10 @@ name: "CodeQL" on: push: - branches: [ master, release/* ] + branches: [ main, release/* ] pull_request: # The branches below must be a subset of the branches above - branches: [ master ] + branches: [ main ] schedule: - cron: '16 10 * * 0' @@ -36,7 +36,7 @@ jobs: strategy: fail-fast: false matrix: - language: [ 'java' ] + 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 @@ -46,7 +46,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + 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. @@ -57,7 +57,7 @@ jobs: # 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@v1 + uses: github/codeql-action/autobuild@v3 # â„šī¸ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -71,4 +71,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + 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 index 0d32e038..8c03a399 100644 --- a/.github/workflows/gradle_push.yml +++ b/.github/workflows/gradle_push.yml @@ -9,7 +9,7 @@ # 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: Build +name: Push on: push: @@ -28,9 +28,13 @@ jobs: with: java-version: '11' distribution: 'temurin' - - name: Build, Check and Coverage + - 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: check jacocoRootReport coveralls \ No newline at end of file + 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/.reuse/dep5 b/.reuse/dep5 deleted file mode 100644 index 96efa937..00000000 --- a/.reuse/dep5 +++ /dev/null @@ -1,71 +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: ... - -# Documentation -Files: docs/* -Copyright: 2022 Paul Schaub -License: CC-BY-3.0 - -Files: .readthedocs.yaml -Copyright: 2022 Paul Schaub -License: CC0-1.0 - -# 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/pgpainless.svg -Copyright: 2021 Paul Schaub -License: CC-BY-3.0 - -Files: assets/logo.png -Copyright: 2022 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 - -# GH Pages -Files: CNAME -Copyright: 2022 Paul Schaub -License: CC0-1.0 - -Files: _config.yml -Copyright: 2022 Paul Schaub -License: CC0-1.0 - -Files: _layouts/* -Copyright: 2022 Paul Schaub , 2017 Steve Smith -License: CC-BY-SA-3.0 - -# Man Pages -Files: pgpainless-cli/rewriteManPages.sh -Copyright: 2022 Paul Schaub -License: Apache-2.0 - -Files: pgpainless-cli/packaging/man/* -Copyright: 2022 Paul Schaub -License: Apache-2.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index bd7f3505..13c93c07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,211 @@ 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 @@ -74,6 +279,14 @@ SPDX-License-Identifier: CC0-1.0 - 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` diff --git a/README.md b/README.md index 2a91789e..e305e43a 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ SPDX-License-Identifier: Apache-2.0 [![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/%232/green)](https://tests.sequoia-pgp.org/) +[![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) @@ -32,7 +32,7 @@ It also checks if signing subkeys are properly bound to their primary key, if ke 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. @@ -132,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() @@ -172,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)!!! @@ -191,7 +191,7 @@ repositories { } dependencies { - implementation 'org.pgpainless:pgpainless-core:1.4.4' + implementation 'org.pgpainless:pgpainless-core:1.7.6' } ``` @@ -222,9 +222,6 @@ Parts of PGPainless development ([project page](https://nlnet.nl/project/PGPainl 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/) -Thanks to [YourKit](https://www.yourkit.com/) for providing a free license of the [YourKit Java Profiler](https://www.yourkit.com/java/profiler/) to support PGPainless Development! -[![YourKit Logo](https://www.yourkit.com/images/yklogo.png)](https://www.yourkit.com/) - Big thank you also to those who decided to support the work by donating! Notably @msfjarvis 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 index 0549d1a0..a05079d3 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -12,11 +12,14 @@ SPDX-License-Identifier: Apache-2.0 Use this section to tell people about which versions of your project are currently being supported with security updates. -| Version | Supported | -|---------|--------------------| -| 1.4.X | :white_check_mark: | -| 1.3.X | :white_check_mark: | -| < 1.3.X | :x: | +| 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 diff --git a/build.gradle b/build.gradle index eb4ff723..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,37 +30,32 @@ allprojects { apply plugin: 'eclipse' apply plugin: 'jacoco' apply plugin: 'checkstyle' + apply plugin: 'kotlin' + apply plugin: 'com.diffplug.spotless' // 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() } } - // For library modules, enable android api compatibility check - if (it.name != 'pgpainless-cli') { - // 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] - } - } - // 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() @@ -74,6 +70,17 @@ allprojects { 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 { rootConfigDir = new File(rootDir, 'config') gitCommit = getGitCommit() @@ -99,7 +106,7 @@ allprojects { } jacoco { - toolVersion = "0.8.7" + toolVersion = "0.8.8" } jacocoTestReport { @@ -107,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 } } @@ -125,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 } @@ -230,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 @@ -241,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') @@ -258,18 +261,6 @@ task javadocAll(type: Javadoc) { ] as String[] } -if (JavaVersion.current().isJava8Compatible()) { - tasks.withType(Javadoc) { - // The '-quiet' as second argument is actually a hack, - // since the one paramater addStringOption doesn't seem to - // work, we extra add '-quiet', which is added anyway by - // gradle. See https://github.com/gradle/gradle/issues/2354 - // See JDK-8200363 (https://bugs.openjdk.java.net/browse/JDK-8200363) - // for information about the -Xwerror option. - options.addStringOption('Xwerror', '-quiet') - } -} - /** * Fetch sha256 checksums of artifacts published to maven central. * @@ -279,34 +270,13 @@ task mavenCentralChecksums() { description 'Fetch and display checksums for artifacts published to Maven Central' String ver = project.hasProperty('release') ? release : shortVersion doLast { - Process p = "curl -f https://repo1.maven.org/maven2/org/pgpainless/pgpainless-core/${ver}/pgpainless-core-${ver}.jar.sha256".execute() - if (p.waitFor() == 0) { - print p.text.trim() - println " pgpainless-core/build/libs/pgpainless-core-${ver}.jar" - } - - p = "curl -f https://repo1.maven.org/maven2/org/pgpainless/pgpainless-sop/${ver}/pgpainless-sop-${ver}.jar.sha256".execute() - if (p.waitFor() == 0) { - print p.text.trim() - println " pgpainless-sop/build/libs/pgpainless-sop-${ver}.jar" - } - - p = "curl -f https://repo1.maven.org/maven2/org/pgpainless/pgpainless-cli/${ver}/pgpainless-cli-${ver}-all.jar.sha256".execute() - if (p.waitFor() == 0) { - print p.text.trim() - println " pgpainless-cli/build/libs/pgpainless-cli-${ver}-all.jar" - } - - p = "curl -f https://repo1.maven.org/maven2/org/pgpainless/pgpainless-cli/${ver}/pgpainless-cli-${ver}.jar.sha256".execute() - if (p.waitFor() == 0) { - print p.text.trim() - println " pgpainless-cli/build/libs/pgpainless-cli-${ver}.jar" - } - - p = "curl -f https://repo1.maven.org/maven2/org/pgpainless/hsregex/${ver}/hsregex-${ver}.jar.sha256".execute() - if (p.waitFor() == 0) { - print p.text.trim() - println " hsregex/build/libs/hsregex-${ver}.jar" + 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/requirements.txt b/docs/requirements.txt index 1d5e0d5b..ab1cf848 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,2 +1,3 @@ myst-parser>=0.17 sphinxcontrib-mermaid>=0.7.1 +sphinx_rtd_theme>=2.0.0 diff --git a/docs/source/ecosystem.md b/docs/source/ecosystem.md index cd88cfd8..e5294b9b 100644 --- a/docs/source/ecosystem.md +++ b/docs/source/ecosystem.md @@ -47,6 +47,13 @@ The diagram below shows, how the different projects relate to one another. * `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 index 6469faaa..bc4bd190 100644 --- a/docs/source/ecosystem_dia.md +++ b/docs/source/ecosystem_dia.md @@ -27,6 +27,13 @@ flowchart LR 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 diff --git a/docs/source/ecosystem_dia.png b/docs/source/ecosystem_dia.png index 70872a9f..0efa16a8 100644 Binary files a/docs/source/ecosystem_dia.png and b/docs/source/ecosystem_dia.png differ diff --git a/docs/source/ecosystem_dia.svg b/docs/source/ecosystem_dia.svg index f242a5c0..7ace9a54 100644 --- a/docs/source/ecosystem_dia.svg +++ b/docs/source/ecosystem_dia.svg @@ -1 +1 @@ -
PGPEASY
VKS-JAVA
CERT-D-PGPAINLESS
CERT-D-JAVA
WKD-JAVA
PGPAINLESS
SOP-JAVA
pgpeasy
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 +
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/pgpainless-cli/usage.md b/docs/source/pgpainless-cli/usage.md index 4bc1d166..15a87846 100644 --- a/docs/source/pgpainless-cli/usage.md +++ b/docs/source/pgpainless-cli/usage.md @@ -82,23 +82,26 @@ Stateless OpenPGP Protocol Usage: pgpainless-cli [--stacktrace] [COMMAND] Options: - --stacktrace Print Stacktrace + --stacktrace Print stacktrace Commands: - help Display usage information for the specified subcommand - armor Add ASCII Armor to standard input - dearmor Remove ASCII Armor from standard input - decrypt Decrypt a message from standard input - inline-detach Split signatures from a clearsigned message - encrypt Encrypt a message from standard input - extract-cert Extract a public key certificate from a secret key from - standard input - generate-key Generate a secret key - sign Create a detached signature on the data from standard input - verify Verify a detached signature over the data from standard input - inline-sign Create an inline-signed message from data on standard input - inline-verify Verify inline-signed data from standard input - version Display version information about the tool + 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 @@ -120,6 +123,9 @@ Exit Codes: 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 diff --git a/docs/source/pgpainless-core/quickstart.md b/docs/source/pgpainless-core/quickstart.md index 5382923f..1816983f 100644 --- a/docs/source/pgpainless-core/quickstart.md +++ b/docs/source/pgpainless-core/quickstart.md @@ -50,9 +50,15 @@ There is a very good chance that you can find code examples there that fit your 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: @@ -231,6 +237,15 @@ EncryptionOptions encOptions = EncryptionOptions.get() 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")); diff --git a/docs/source/pgpainless-sop/quickstart.md b/docs/source/pgpainless-sop/quickstart.md index 10ef0a72..55958ee4 100644 --- a/docs/source/pgpainless-sop/quickstart.md +++ b/docs/source/pgpainless-sop/quickstart.md @@ -75,10 +75,21 @@ In both cases, the resulting output will be the UTF8 encoded, ASCII armored Open To disable ASCII armoring, call `noArmor()` before calling `generate()`. -At the time of writing, the resulting OpenPGP secret key will consist of a certification-capable 256-bits +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 @@ -103,6 +114,56 @@ 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, @@ -119,14 +180,6 @@ byte[] armoredData = sop.armor() The `data(_)` method can either be called by providing a byte array, or an `InputStream`. -:::{note} -There is a `label(ArmorLabel label)` method, which could theoretically be used to define the label used in the -ASCII armor header. -However, this method is not (yet?) supported by `pgpainless-sop` and will currently throw an `UnsupportedOption` -exception. -Instead, the implementation will figure out the data type and set the respective label on its own. -::: - To remove ASCII armor from armored data, simply use the `dearmor()` API: ```java @@ -186,6 +239,13 @@ If any keys used for signing are password protected, you need to provide the sig 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. @@ -464,3 +524,23 @@ By default, the signatures output will be ASCII armored. This can be disabled by 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/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 8049c684..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-7.5-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 index d04160cc..83217b7d 100644 --- a/misc/OpenPGPMessageFormat.md +++ b/misc/OpenPGPMessageFormat.md @@ -67,6 +67,7 @@ graph LR 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 diff --git a/pgpainless-cli/build.gradle b/pgpainless-cli/build.gradle index 3d9a6a09..e4c3f060 100644 --- a/pgpainless-cli/build.gradle +++ b/pgpainless-cli/build.gradle @@ -4,27 +4,13 @@ plugins { id 'application' - id "com.github.johnrengelman.shadow" version "6.1.0" -} -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 { @@ -32,13 +18,12 @@ 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.2' // 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 "ch.qos.logback:logback-classic:$logbackVersion" + implementation "org.slf4j:slf4j-nop:$slf4jVersion" implementation(project(":pgpainless-sop")) implementation "org.pgpainless:sop-java-picocli:$sopJavaVersion" @@ -52,22 +37,6 @@ mainClassName = 'org.pgpainless.cli.PGPainlessCLI' application { mainClass = mainClassName } -/** -jar { - duplicatesStrategy(DuplicatesStrategy.EXCLUDE) - 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" - } -} - */ run { // https://stackoverflow.com/questions/59445306/pipe-into-gradle-run @@ -77,5 +46,3 @@ run { args Eval.me(appArgs) } } - -// tasks."jar".dependsOn(":pgpainless-core:assemble", ":pgpainless-sop:assemble") diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-armor.1 b/pgpainless-cli/packaging/man/pgpainless-cli-armor.1 index c34cf923..0d948dfd 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-armor.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-armor.1 @@ -30,16 +30,12 @@ pgpainless\-cli\-armor \- Add ASCII Armor to standard input .SH "SYNOPSIS" .sp -\fBpgpainless\-cli armor\fP [\fB\-\-stacktrace\fP] [\fB\-\-label\fP=\fI{auto|sig|key|cert|message}\fP] +\fBpgpainless\-cli armor\fP [\fB\-\-stacktrace\fP] .SH "DESCRIPTION" .SH "OPTIONS" .sp -\fB\-\-label\fP=\fI{auto|sig|key|cert|message}\fP -.RS 4 -Label to be used in the header and tail of the armoring -.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-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 index 19ef25f1..85f9277d 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-dearmor.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-dearmor.1 @@ -37,4 +37,5 @@ pgpainless\-cli\-dearmor \- Remove ASCII Armor from standard input .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 index 17d59134..eb843e18 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-decrypt.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-decrypt.1 @@ -27,19 +27,29 @@ . LINKSTYLE blue R < > .\} .SH "NAME" -pgpainless\-cli\-decrypt \- Decrypt a message from standard input +pgpainless\-cli\-decrypt \- Decrypt a message .SH "SYNOPSIS" .sp -\fBpgpainless\-cli decrypt\fP [\fB\-\-stacktrace\fP] [\fB\-\-not\-after\fP=\fIDATE\fP] [\fB\-\-not\-before\fP=\fIDATE\fP] -[\fB\-\-session\-key\-out\fP=\fISESSIONKEY\fP] [\fB\-\-verify\-out\fP=\fIVERIFICATIONS\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...] +\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\-\-not\-after\fP=\fIDATE\fP +\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 @@ -50,7 +60,7 @@ Defaults to current system time (\(aqnow\(aq). Accepts special value \(aq\-\(aq for end of time. .RE .sp -\fB\-\-not\-before\fP=\fIDATE\fP +\fB\-\-verify\-not\-before\fP=\fIDATE\fP .RS 4 ISO\-8601 formatted UTC date (e.g. \(aq2020\-11\-23T16:35Z) .sp @@ -59,16 +69,6 @@ Reject signatures with a creation date not in range. Defaults to beginning of time (\(aq\-\(aq). .RE .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, \fB\-\-verify\-out, \-\-verifications\-out\fP=\fIVERIFICATIONS\fP -.RS 4 -Emits signature verification status to the designated output -.RE -.sp \fB\-\-verify\-with\fP=\fICERT\fP .RS 4 Certificates for signature verification @@ -87,7 +87,7 @@ 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...) +Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). .RE .sp \fB\-\-with\-session\-key\fP=\fISESSIONKEY\fP @@ -96,7 +96,7 @@ 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...) +Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). .RE .SH "ARGUMENTS" .sp diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-encrypt.1 b/pgpainless-cli/packaging/man/pgpainless-cli-encrypt.1 index f1d804d0..3cf7aefc 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-encrypt.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-encrypt.1 @@ -31,6 +31,7 @@ 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" @@ -47,12 +48,22 @@ Type of the input data. Defaults to \(aqbinary\(aq ASCII armor the output .RE .sp -\fB\-\-sign\-with\fP=\fIKEY\fP +\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, \fB\-\-with\-key\-password\fP=\fIPASSWORD\fP +\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 @@ -63,7 +74,7 @@ Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). .RS 4 Encrypt the message with a password. .sp -Is an INDIRECT data type (e.g. file, environment variable, file descriptor...) +Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). .RE .SH "ARGUMENTS" .sp diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-extract-cert.1 b/pgpainless-cli/packaging/man/pgpainless-cli-extract-cert.1 index dcaf6e71..db8f7619 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-extract-cert.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-extract-cert.1 @@ -27,12 +27,13 @@ . LINKSTYLE blue R < > .\} .SH "NAME" -pgpainless\-cli\-extract\-cert \- Extract a public key certificate from a secret key from standard input +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 @@ -42,4 +43,5 @@ ASCII armor the output .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 index 5ab3d673..5c50ee96 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-generate-completion.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-generate-completion.1 @@ -152,4 +152,14 @@ Ambiguous input (a filename matching the designator already exists) \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 index 96b069f3..f72332fa 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-generate-key.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-generate-key.1 @@ -30,8 +30,8 @@ pgpainless\-cli\-generate\-key \- Generate a secret key .SH "SYNOPSIS" .sp -\fBpgpainless\-cli generate\-key\fP [\fB\-\-[no\-]armor\fP] [\fB\-\-stacktrace\fP] [\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP] -[\fIUSERID\fP...] +\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" @@ -41,7 +41,22 @@ pgpainless\-cli\-generate\-key \- Generate a secret key ASCII armor the output .RE .sp -\fB\-\-stacktrace\fP, \fB\-\-with\-key\-password\fP=\fIPASSWORD\fP +\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 diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-help.1 b/pgpainless-cli/packaging/man/pgpainless-cli-help.1 index 6152fc87..1e7c2b08 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-help.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-help.1 @@ -147,4 +147,14 @@ Ambiguous input (a filename matching the designator already exists) \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 index c5d9d983..4463bedb 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-inline-detach.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-inline-detach.1 @@ -47,4 +47,5 @@ Destination to which a detached signatures block will be written .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 index 7deb568c..4ee06ec5 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-inline-sign.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-inline-sign.1 @@ -27,23 +27,22 @@ . LINKSTYLE blue R < > .\} .SH "NAME" -pgpainless\-cli\-inline\-sign \- Create an inline\-signed message from data on standard input +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|cleartextsigned}\fP] +\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|cleartextsigned}\fP +\fB\-\-as\fP=\fI{binary|text|clearsigned}\fP .RS 4 -Specify the signature format of the signed message +Specify the signature format of the signed message. .sp \(aqtext\(aq and \(aqbinary\(aq will produce inline\-signed messages. .sp -\(aqcleartextsigned\(aq will make use of the cleartext signature framework. +\(aqclearsigned\(aq will make use of the cleartext signature framework. .sp Defaults to \(aqbinary\(aq. .sp @@ -55,7 +54,12 @@ If \(aq\-\-as=text\(aq and the input data is not valid UTF\-8, inline\-sign fail ASCII armor the output .RE .sp -\fB\-\-stacktrace\fP, \fB\-\-with\-key\-password\fP=\fIPASSWORD\fP +\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 diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-inline-verify.1 b/pgpainless-cli/packaging/man/pgpainless-cli-inline-verify.1 index d97f274d..a2112d44 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-inline-verify.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-inline-verify.1 @@ -27,11 +27,11 @@ . LINKSTYLE blue R < > .\} .SH "NAME" -pgpainless\-cli\-inline\-verify \- Verify inline\-signed data from standard input +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=\fI\fP] [\fICERT\fP...] +[\fB\-\-verifications\-out\fP=\fIVERIFICATIONS\fP] [\fICERT\fP...] .SH "DESCRIPTION" .SH "OPTIONS" @@ -56,7 +56,12 @@ Reject signatures with a creation date not in range. Defaults to beginning of time ("\-"). .RE .sp -\fB\-\-stacktrace\fP, \fB\-\-verifications\-out\fP=\fI\fP +\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 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 index 6519e0ec..d975e318 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-sign.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-sign.1 @@ -27,7 +27,7 @@ . LINKSTYLE blue R < > .\} .SH "NAME" -pgpainless\-cli\-sign \- Create a detached signature on the data from standard input +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] @@ -38,7 +38,7 @@ pgpainless\-cli\-sign \- Create a detached signature on the data from standard i .sp \fB\-\-as\fP=\fI{binary|text}\fP .RS 4 -Specify the output format of the signed message +Specify the output format of the signed message. .sp Defaults to \(aqbinary\(aq. .sp @@ -47,7 +47,7 @@ If \(aq\-\-as=text\(aq and the input data is not valid UTF\-8, sign fails with r .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) +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 @@ -55,7 +55,12 @@ Emits the digest algorithm used to the specified file in a way that can be used ASCII armor the output .RE .sp -\fB\-\-stacktrace\fP, \fB\-\-with\-key\-password\fP=\fIPASSWORD\fP +\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 diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-verify.1 b/pgpainless-cli/packaging/man/pgpainless-cli-verify.1 index 5cf0020c..12ede1e1 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-verify.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-verify.1 @@ -27,16 +27,28 @@ . LINKSTYLE blue R < > .\} .SH "NAME" -pgpainless\-cli\-verify \- Verify a detached signature over the data from standard input +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, \fB\-\-not\-before\fP=\fIDATE\fP +\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 @@ -47,6 +59,7 @@ Defaults to beginning of time ("\-"). .sp \fB\-\-stacktrace\fP .RS 4 +Print stacktrace .RE .SH "ARGUMENTS" .sp @@ -57,4 +70,5 @@ Detached signature .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 index 003e549f..c552bbe0 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-version.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-version.1 @@ -30,7 +30,7 @@ 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] +\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" @@ -45,6 +45,12 @@ Print information about the cryptographic backend Print an extended version string .RE .sp -\fB\-\-stacktrace\fP +\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 index 686f728f..f23468da 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli.1 @@ -41,9 +41,69 @@ Print stacktrace .RE .SH "COMMANDS" .sp -\fBhelp\fP +\fBversion\fP .RS 4 -Stateless OpenPGP Protocol +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 @@ -56,54 +116,9 @@ Add ASCII Armor to standard input Remove ASCII Armor from standard input .RE .sp -\fBdecrypt\fP +\fBhelp\fP .RS 4 -Decrypt a message from standard input -.RE -.sp -\fBinline\-detach\fP -.RS 4 -Split signatures from a clearsigned message -.RE -.sp -\fBencrypt\fP -.RS 4 -Encrypt a message from standard input -.RE -.sp -\fBextract\-cert\fP -.RS 4 -Extract a public key certificate from a secret key from standard input -.RE -.sp -\fBgenerate\-key\fP -.RS 4 -Generate a secret key -.RE -.sp -\fBsign\fP -.RS 4 -Create a detached signature on the data from standard input -.RE -.sp -\fBverify\fP -.RS 4 -Verify a detached signature over the data from standard input -.RE -.sp -\fBinline\-sign\fP -.RS 4 -Create an inline\-signed message from data on standard input -.RE -.sp -\fBinline\-verify\fP -.RS 4 -Verify inline\-signed data from standard input -.RE -.sp -\fBversion\fP -.RS 4 -Display version information about the tool +Stateless OpenPGP Protocol .RE .sp \fBgenerate\-completion\fP @@ -205,4 +220,14 @@ Ambiguous input (a filename matching the designator already exists) \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 index 321dbdde..730af68d 100755 --- a/pgpainless-cli/rewriteManPages.sh +++ b/pgpainless-cli/rewriteManPages.sh @@ -4,7 +4,7 @@ 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." && exit 1; +[ ! -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 @@ -13,12 +13,14 @@ 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 938bf1aa..7625dd17 100644 --- a/pgpainless-cli/src/main/java/org/pgpainless/cli/PGPainlessCLI.java +++ b/pgpainless-cli/src/main/java/org/pgpainless/cli/PGPainlessCLI.java @@ -14,6 +14,10 @@ import sop.cli.picocli.SopCLI; 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()); } 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 559589ef..8451d6a4 100644 --- a/pgpainless-cli/src/main/resources/logback.xml +++ b/pgpainless-cli/src/main/resources/logback.xml @@ -5,22 +5,5 @@ SPDX-License-Identifier: Apache-2.0 --> - - 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-cli/src/test/java/org/pgpainless/cli/ExitCodeTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/ExitCodeTest.java index 07c9bf68..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,28 +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(SOPGPException.UnsupportedSubcommand.EXIT_CODE) - 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(SOPGPException.UnsupportedOption.EXIT_CODE) - 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/commands/ArmorCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ArmorCmdTest.java index afd5ded4..ddb3c6dd 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ArmorCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ArmorCmdTest.java @@ -16,7 +16,6 @@ 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 ArmorCmdTest extends CLITest { @@ -89,15 +88,6 @@ public class ArmorCmdTest extends CLITest { assertTrue(armored.contains("SGVsbG8sIFdvcmxkIQo=")); } - @Test - public void labelNotYetSupported() throws IOException { - pipeStringToStdin("Hello, World!\n"); - ByteArrayOutputStream out = pipeStdoutToStream(); - int exitCode = executeCommand("armor", "--label", "Message"); - assertEquals(SOPGPException.UnsupportedOption.EXIT_CODE, exitCode); - assertEquals(0, out.size()); - } - @Test public void armorAlreadyArmoredDataIsIdempotent() throws IOException { pipeStringToStdin(key); 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 index 8854d837..19bc9aa5 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/InlineDetachCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/InlineDetachCmdTest.java @@ -90,7 +90,7 @@ public class InlineDetachCmdTest extends CLITest { pipeStringToStdin(msgOut.toString()); ByteArrayOutputStream verifyOut = pipeStdoutToStream(); assertSuccess(executeCommand("verify", sigFile.getAbsolutePath(), certFile.getAbsolutePath())); - assertEquals("2021-05-15T16:08:06Z 4F665C4DC2C4660BC6425E415736E6931ACF370C 4F665C4DC2C4660BC6425E415736E6931ACF370C\n", + assertEquals("2021-05-15T16:08:06Z 4F665C4DC2C4660BC6425E415736E6931ACF370C 4F665C4DC2C4660BC6425E415736E6931ACF370C mode:text\n", verifyOut.toString()); } @@ -115,7 +115,7 @@ public class InlineDetachCmdTest extends CLITest { ByteArrayOutputStream verifyOut = pipeStdoutToStream(); File certFile = writeFile("cert.asc", CERT); assertSuccess(executeCommand("verify", sigFile.getAbsolutePath(), certFile.getAbsolutePath())); - assertEquals("2021-05-15T16:08:06Z 4F665C4DC2C4660BC6425E415736E6931ACF370C 4F665C4DC2C4660BC6425E415736E6931ACF370C\n", + assertEquals("2021-05-15T16:08:06Z 4F665C4DC2C4660BC6425E415736E6931ACF370C 4F665C4DC2C4660BC6425E415736E6931ACF370C mode:text\n", verifyOut.toString()); } 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 index d314de20..f8d56bc3 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripEncryptDecryptCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripEncryptDecryptCmdTest.java @@ -14,17 +14,17 @@ import java.io.IOException; 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.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.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.slf4j.LoggerFactory; import sop.exception.SOPGPException; @@ -82,7 +82,6 @@ public class RoundTripEncryptDecryptCmdTest extends CLITest { "-----END PGP PUBLIC KEY BLOCK-----"; @Test - @FailOnSystemExit public void encryptAndDecryptAMessage() throws IOException { // Juliets key and cert File julietKeyFile = pipeStdoutToFile("juliet.key"); @@ -129,7 +128,7 @@ public class RoundTripEncryptDecryptCmdTest extends CLITest { String romeosVerif = readStringFromFile(anotherVerificationsFile); assertEquals(julietsVerif, romeosVerif); assertFalse(julietsVerif.isEmpty()); - assertEquals(103, julietsVerif.length()); // 103 is number of symbols in [DATE, FINGER, FINGER] for V4 + assertEquals(115, julietsVerif.length()); // 115 is number of symbols in [DATE, FINGER, FINGER, MODE] for V4 } @Test @@ -139,6 +138,7 @@ public class RoundTripEncryptDecryptCmdTest extends CLITest { } @Test + @Disabled("Disabled, since we now read certificates from secret keys") public void testEncryptingForKeyFails() throws IOException { File notACert = writeFile("key.asc", KEY); @@ -274,7 +274,7 @@ public class RoundTripEncryptDecryptCmdTest extends CLITest { assertEquals(plaintext, out.toString()); String verificationString = readStringFromFile(verifications); - assertEquals("2022-11-09T17:22:48Z C0DCEC44B1A173664B05DABCECD0BF863F65C9A5 A2EC077FC977E15DD799EFF92C0D3C123CF51C08\n", + assertEquals("2022-11-09T17:22:48Z C0DCEC44B1A173664B05DABCECD0BF863F65C9A5 A2EC077FC977E15DD799EFF92C0D3C123CF51C08 mode:binary\n", verificationString); } @@ -302,7 +302,7 @@ public class RoundTripEncryptDecryptCmdTest extends CLITest { InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() .addUserId("No Crypt ") - .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA)) .build(); PGPPublicKeyRing cert = PGPainless.extractCertificate(secretKeys); @@ -321,9 +321,9 @@ public class RoundTripEncryptDecryptCmdTest extends CLITest { throws IOException, PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() .addUserId("Cannot Sign ") - .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER)) + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER)) .addSubkey(KeySpec.getBuilder( - KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)) + 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()); @@ -644,4 +644,30 @@ public class RoundTripEncryptDecryptCmdTest extends CLITest { 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 index d36ee58f..057cec98 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java @@ -138,6 +138,10 @@ public class RoundTripInlineSignInlineVerifyCmdTest extends CLITest { "\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); @@ -153,7 +157,7 @@ public class RoundTripInlineSignInlineVerifyCmdTest extends CLITest { String cleartextSigned = ciphertextOut.toString(); assertTrue(cleartextSigned.startsWith("-----BEGIN PGP SIGNED MESSAGE-----\n" + "Hash: ")); - assertTrue(cleartextSigned.contains(MESSAGE)); + assertTrue(cleartextSigned.contains(MESSAGE_CRLF)); assertTrue(cleartextSigned.contains("\n-----BEGIN PGP SIGNATURE-----\n")); assertTrue(cleartextSigned.endsWith("-----END PGP SIGNATURE-----\n")); } @@ -203,7 +207,7 @@ public class RoundTripInlineSignInlineVerifyCmdTest extends CLITest { "--verifications-out", verifications.getAbsolutePath(), cert.getAbsolutePath())); - assertEquals(MESSAGE, plaintextOut.toString()); + assertEquals(MESSAGE_CRLF, plaintextOut.toString()); String verificationString = readStringFromFile(verifications); assertTrue(verificationString.contains(CERT_1_SIGNING_KEY)); } @@ -409,7 +413,7 @@ public class RoundTripInlineSignInlineVerifyCmdTest extends CLITest { assertEquals("Hello, World!\n", out.toString()); String ver = readStringFromFile(verifications); assertEquals( - "2022-11-18T14:55:33Z 7A073EDF273C902796D259528FBDD36D01831673 AEA0FD2C899D3FC077815F0026560D2AE53DB86F\n", ver); + "2022-11-18T14:55:33Z 7A073EDF273C902796D259528FBDD36D01831673 AEA0FD2C899D3FC077815F0026560D2AE53DB86F mode:binary\n", ver); } @Test 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 index 82fda430..cf350e66 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignVerifyCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignVerifyCmdTest.java @@ -4,104 +4,49 @@ 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.assertFalse; -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; +import org.slf4j.LoggerFactory; -public class RoundTripInlineSignVerifyCmdTest { - private static File tempDir; - private static PrintStream originalSout; +public class RoundTripInlineSignVerifyCmdTest extends CLITest { - @BeforeAll - public static void prepare() throws IOException { - tempDir = TestUtils.createTempDirectory(); + public RoundTripInlineSignVerifyCmdTest() { + super(LoggerFactory.getLogger(RoundTripInlineSignVerifyCmdTest.class)); } @Test - @FailOnSystemExit public void encryptAndDecryptAMessage() throws IOException { - originalSout = System.out; - File sigmundKeyFile = new File(tempDir, "sigmund.key"); - assertTrue(sigmundKeyFile.createNewFile()); - - File sigmundCertFile = new File(tempDir, "sigmund.cert"); - assertTrue(sigmundCertFile.createNewFile()); - - File msgFile = new File(tempDir, "signed.asc"); - assertTrue(msgFile.createNewFile()); - - File passwordFile = new File(tempDir, "password"); - assertTrue(passwordFile.createNewFile()); - // write password file - FileOutputStream passwordOut = new FileOutputStream(passwordFile); - passwordOut.write("sw0rdf1sh".getBytes(StandardCharsets.UTF_8)); - passwordOut.close(); + File password = writeFile("password", "sw0rdf1sh"); // generate key - OutputStream sigmundKeyOut = new FileOutputStream(sigmundKeyFile); - System.setOut(new PrintStream(sigmundKeyOut)); - PGPainlessCLI.execute("generate-key", - "--with-key-password=" + passwordFile.getAbsolutePath(), - "Sigmund Freud "); - sigmundKeyOut.close(); + File sigmundKey = pipeStdoutToFile("sigmund.key"); + assertSuccess(executeCommand("generate-key", "--with-key-password=" + password.getAbsolutePath(), + "Sigmund Freud ")); // extract cert - FileInputStream sigmundKeyIn = new FileInputStream(sigmundKeyFile); - System.setIn(sigmundKeyIn); - OutputStream sigmundCertOut = new FileOutputStream(sigmundCertFile); - System.setOut(new PrintStream(sigmundCertOut)); - PGPainlessCLI.execute("extract-cert"); - sigmundKeyIn.close(); - sigmundCertOut.close(); + File sigmundCert = pipeStdoutToFile("sigmund.cert"); + pipeFileToStdin(sigmundKey); + assertSuccess(executeCommand("extract-cert")); // sign message - String msg = "Hello World!\n"; - ByteArrayInputStream msgIn = new ByteArrayInputStream(msg.getBytes(StandardCharsets.UTF_8)); - System.setIn(msgIn); - OutputStream msgAscOut = new FileOutputStream(msgFile); - System.setOut(new PrintStream(msgAscOut)); - PGPainlessCLI.execute("inline-sign", - "--with-key-password=" + passwordFile.getAbsolutePath(), - sigmundKeyFile.getAbsolutePath()); - msgAscOut.close(); + pipeBytesToStdin("Hello, World!\n".getBytes(StandardCharsets.UTF_8)); + File signedMsg = pipeStdoutToFile("signed.asc"); + assertSuccess(executeCommand("inline-sign", "--with-key-password=" + password.getAbsolutePath(), + sigmundKey.getAbsolutePath())); - File verifyFile = new File(tempDir, "verify.txt"); + // verify message + File verifyFile = nonExistentFile("verify.txt"); + pipeFileToStdin(signedMsg); + assertSuccess(executeCommand("inline-verify", "--verifications-out", verifyFile.getAbsolutePath(), + sigmundCert.getAbsolutePath())); - FileInputStream msgAscIn = new FileInputStream(msgFile); - System.setIn(msgAscIn); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - PrintStream pOut = new PrintStream(out); - System.setOut(pOut); - PGPainlessCLI.execute("inline-verify", - "--verifications-out", verifyFile.getAbsolutePath(), - sigmundCertFile.getAbsolutePath()); - msgAscIn.close(); - - assertEquals(msg, out.toString()); - } - - @AfterAll - public static void after() { - System.setOut(originalSout); - // CHECKSTYLE:OFF - System.out.println(tempDir.getAbsolutePath()); - // CHECKSTYLE:ON + 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 index 6196a847..9dcb3aca 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripSignVerifyCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripSignVerifyCmdTest.java @@ -13,6 +13,7 @@ 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; @@ -24,8 +25,8 @@ 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.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.info.KeyRingInfo; import org.slf4j.LoggerFactory; import sop.exception.SOPGPException; @@ -94,7 +95,7 @@ public class RoundTripSignVerifyCmdTest extends CLITest { "=VWAZ\n" + "-----END PGP SIGNATURE-----"; private static final String BINARY_SIG_VERIFICATION = - "2022-11-09T18:40:24Z 444C10AB011EF8424C83F0A9DA9F413986211DC6 9DA09423C9F94BA4CCA30951099B11BF296A373E\n"; + "2022-11-09T18:40:24Z 444C10AB011EF8424C83F0A9DA9F413986211DC6 9DA09423C9F94BA4CCA30951099B11BF296A373E mode:binary\n"; private static final String TEXT_SIG = "-----BEGIN PGP SIGNATURE-----\n" + "Version: PGPainless\n" + "\n" + @@ -104,8 +105,16 @@ public class RoundTripSignVerifyCmdTest extends CLITest { "=s5xn\n" + "-----END PGP SIGNATURE-----"; private static final String TEXT_SIG_VERIFICATION = - "2022-11-09T18:41:18Z 444C10AB011EF8424C83F0A9DA9F413986211DC6 9DA09423C9F94BA4CCA30951099B11BF296A373E\n"; - private static final Date TEXT_SIG_CREATION = UTCUtil.parseUTCDate("2022-11-09T18:41:18Z"); + "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 { @@ -193,8 +202,8 @@ public class RoundTripSignVerifyCmdTest extends CLITest { throws IOException, PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() .addUserId("Cannot Sign ") - .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER)) - .addSubkey(KeySpec.getBuilder(KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)) + .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()); 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 index 2e4aa7e4..e0915920 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/VersionCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/VersionCmdTest.java @@ -29,7 +29,7 @@ public class VersionCmdTest extends CLITest { public void testGetBackendVersion() throws IOException { ByteArrayOutputStream out = pipeStdoutToStream(); assertSuccess(executeCommand("version", "--backend")); - assertTrue(out.toString().startsWith("Bouncy Castle ")); + assertTrue(out.toString().startsWith("PGPainless ")); } @Test @@ -41,4 +41,12 @@ public class VersionCmdTest extends CLITest { 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 index affe621e..d6065b82 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/misc/SignUsingPublicKeyBehaviorTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/misc/SignUsingPublicKeyBehaviorTest.java @@ -4,28 +4,18 @@ package org.pgpainless.cli.misc; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; -import java.io.ByteArrayInputStream; 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 com.ginsberg.junit.exit.ExpectSystemExitWithStatus; -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.cli.PGPainlessCLI; -import org.pgpainless.cli.TestUtils; +import org.pgpainless.cli.commands.CLITest; +import org.slf4j.LoggerFactory; import sop.exception.SOPGPException; -public class SignUsingPublicKeyBehaviorTest { +public class SignUsingPublicKeyBehaviorTest extends CLITest { public static final String KEY_THAT_IS_A_CERT = "" + "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + @@ -89,61 +79,24 @@ public class SignUsingPublicKeyBehaviorTest { "=oJQ2\n" + "-----END PGP PUBLIC KEY BLOCK-----"; - - private static File tempDir; - private static PrintStream originalSout; - - @BeforeAll - public static void prepare() throws IOException { - tempDir = TestUtils.createTempDirectory(); + public SignUsingPublicKeyBehaviorTest() { + super(LoggerFactory.getLogger(SignUsingPublicKeyBehaviorTest.class)); } @Test - @ExpectSystemExitWithStatus(SOPGPException.KeyCannotSign.EXIT_CODE) public void testSignatureCreationAndVerification() throws IOException { - originalSout = System.out; - InputStream originalIn = System.in; - // Write alice key to disc - File aliceKeyFile = new File(tempDir, "alice.key"); - assertTrue(aliceKeyFile.createNewFile()); - OutputStream aliceKeyOut = new FileOutputStream(aliceKeyFile); - Streams.pipeAll(new ByteArrayInputStream(KEY_THAT_IS_A_CERT.getBytes(StandardCharsets.UTF_8)), aliceKeyOut); - aliceKeyOut.close(); - - // Write alice pub key to disc - File aliceCertFile = new File(tempDir, "alice.pub"); - assertTrue(aliceCertFile.createNewFile()); - OutputStream aliceCertOut = new FileOutputStream(aliceCertFile); - Streams.pipeAll(new ByteArrayInputStream(KEY_THAT_IS_A_CERT.getBytes(StandardCharsets.UTF_8)), aliceCertOut); - aliceCertOut.close(); + File aliceKeyFile = writeFile("alice.key", KEY_THAT_IS_A_CERT); // Write test data to disc - String data = "If privacy is outlawed, only outlaws will have privacy.\n"; - - 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(); + File dataFile = writeFile("data", "If privacy is outlawed, only outlaws will have privacy.\n"); // 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.main(new String[] {"sign", "--armor", aliceKeyFile.getAbsolutePath()}); + File sigFile = pipeStdoutToFile("sig.asc"); + pipeFileToStdin(dataFile); + assertEquals(SOPGPException.KeyCannotSign.EXIT_CODE, + executeCommand("sign", "--armor", aliceKeyFile.getAbsolutePath())); - System.setIn(originalIn); - } - - @AfterAll - public static void after() { - System.setOut(originalSout); - // CHECKSTYLE:OFF - System.out.println(tempDir.getAbsolutePath()); - // CHECKSTYLE:ON + assertTrue(readStringFromFile(sigFile).trim().isEmpty()); } } diff --git a/pgpainless-core/build.gradle b/pgpainless-core/build.gradle index 3c73121f..64d538d5 100644 --- a/pgpainless-core/build.gradle +++ b/pgpainless-core/build.gradle @@ -20,10 +20,18 @@ dependencies { testImplementation "ch.qos.logback:logback-classic:$logbackVersion" // Bouncy Castle - api "org.bouncycastle:bcprov-jdk15to18:$bouncyCastleVersion" - api "org.bouncycastle:bcpg-jdk15to18:$bouncyPgVersion" + 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")) // @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/bouncycastle/CachingBcPublicKeyDataDecryptorFactory.java b/pgpainless-core/src/main/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactory.java deleted file mode 100644 index 510b0938..00000000 --- a/pgpainless-core/src/main/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactory.java +++ /dev/null @@ -1,95 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.bouncycastle; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPrivateKey; -import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; -import org.bouncycastle.openpgp.operator.bc.BcPublicKeyDataDecryptorFactory; -import org.bouncycastle.util.encoders.Base64; -import org.bouncycastle.util.encoders.Hex; -import org.pgpainless.decryption_verification.CustomPublicKeyDataDecryptorFactory; -import org.pgpainless.key.SubkeyIdentifier; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.HashMap; -import java.util.Map; - -/** - * Implementation of the {@link 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 {@link #recoverSessionData(int, byte[][])} to first return any - * cache hits. - * If no hit is found, the method call is delegated to the underlying {@link PublicKeyDataDecryptorFactory}. - * The result of that is then placed in the cache and returned. - */ -public class CachingBcPublicKeyDataDecryptorFactory - extends BcPublicKeyDataDecryptorFactory - implements CustomPublicKeyDataDecryptorFactory { - - private static final Logger LOGGER = LoggerFactory.getLogger(CachingBcPublicKeyDataDecryptorFactory.class); - - private final Map cachedSessionKeys = new HashMap<>(); - private final SubkeyIdentifier decryptionKey; - - public CachingBcPublicKeyDataDecryptorFactory(PGPPrivateKey privateKey, SubkeyIdentifier decryptionKey) { - super(privateKey); - this.decryptionKey = decryptionKey; - } - - @Override - public byte[] recoverSessionData(int keyAlgorithm, byte[][] secKeyData) throws PGPException { - byte[] sessionKey = lookupSessionKeyData(secKeyData); - if (sessionKey == null) { - LOGGER.debug("Cache miss for encrypted session key " + Hex.toHexString(secKeyData[0])); - sessionKey = costlyRecoverSessionData(keyAlgorithm, secKeyData); - cacheSessionKeyData(secKeyData, sessionKey); - } else { - LOGGER.debug("Cache hit for encrypted session key " + Hex.toHexString(secKeyData[0])); - } - return sessionKey; - } - - public byte[] costlyRecoverSessionData(int keyAlgorithm, byte[][] secKeyData) throws PGPException { - return super.recoverSessionData(keyAlgorithm, secKeyData); - } - - private byte[] lookupSessionKeyData(byte[][] secKeyData) { - String key = toKey(secKeyData); - byte[] sessionKey = cachedSessionKeys.get(key); - return copy(sessionKey); - } - - private void cacheSessionKeyData(byte[][] secKeyData, byte[] sessionKey) { - String key = toKey(secKeyData); - cachedSessionKeys.put(key, copy(sessionKey)); - } - - private static String toKey(byte[][] secKeyData) { - byte[] sk = secKeyData[0]; - String key = Base64.toBase64String(sk); - return key; - } - - private static byte[] copy(byte[] bytes) { - if (bytes == null) { - return null; - } - byte[] copy = new byte[bytes.length]; - System.arraycopy(bytes, 0, copy, 0, copy.length); - return copy; - } - - public void clear() { - cachedSessionKeys.clear(); - } - - @Override - public SubkeyIdentifier getSubkeyIdentifier() { - return decryptionKey; - } -} diff --git a/pgpainless-core/src/main/java/org/bouncycastle/package-info.java b/pgpainless-core/src/main/java/org/bouncycastle/package-info.java deleted file mode 100644 index 565bb5f4..00000000 --- a/pgpainless-core/src/main/java/org/bouncycastle/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Classes which could be upstreamed to BC at some point. - */ -package org.bouncycastle; 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 6da77c80..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java +++ /dev/null @@ -1,222 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless; - -import java.io.IOException; -import java.io.OutputStream; -import java.util.Date; -import javax.annotation.Nonnull; - -import org.bouncycastle.bcpg.ArmoredOutputStream; -import org.bouncycastle.openpgp.PGPException; -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.decryption_verification.DecryptionStream; -import org.pgpainless.encryption_signing.EncryptionBuilder; -import org.pgpainless.encryption_signing.EncryptionStream; -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.modification.secretkeyring.SecretKeyRingEditorInterface; -import org.pgpainless.key.parsing.KeyRingReader; -import org.pgpainless.key.util.KeyRingUtils; -import org.pgpainless.policy.Policy; -import org.pgpainless.util.ArmorUtils; - -public final class PGPainless { - - private PGPainless() { - - } - - /** - * Generate a fresh OpenPGP key ring from predefined templates. - * @return templates - */ - public static KeyRingTemplates generateKeyRing() { - return new KeyRingTemplates(); - } - - /** - * Build a custom OpenPGP key ring. - * - * @return builder - */ - public static KeyRingBuilder buildKeyRing() { - 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); - } - - /** - * 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 - */ - public static PGPPublicKeyRing mergeCertificate( - @Nonnull PGPPublicKeyRing originalCopy, - @Nonnull PGPPublicKeyRing updatedCopy) - throws PGPException { - return 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 in the {@link ArmoredOutputStream} - */ - public static String asciiArmor(@Nonnull PGPKeyRing key) - throws IOException { - if (key instanceof PGPSecretKeyRing) { - return ArmorUtils.toAsciiArmoredString((PGPSecretKeyRing) key); - } else { - return ArmorUtils.toAsciiArmoredString((PGPPublicKeyRing) key); - } - } - - /** - * Wrap the detached signature in ASCII armor. - * - * @param signature detached signature - * @return ascii armored string - * - * @throws IOException in case of an error in the {@link ArmoredOutputStream} - */ - public static String asciiArmor(@Nonnull PGPSignature signature) - throws IOException { - return ArmorUtils.toAsciiArmoredString(signature); - } - - /** - * Wrap a key of certificate in ASCII armor and write the result into the given {@link OutputStream}. - * - * @param key key or certificate - * @param outputStream output stream - * - * @throws IOException in case of an error ion the {@link ArmoredOutputStream} - */ - public static void asciiArmor(@Nonnull PGPKeyRing key, @Nonnull OutputStream outputStream) - throws IOException { - ArmoredOutputStream armorOut = ArmorUtils.toAsciiArmoredStream(key, outputStream); - key.encode(armorOut); - armorOut.close(); - } - - /** - * 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(); - } - - /** - * Make changes to a secret key. - * This method can be used to change key expiration dates and passphrases, or add/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 modifyKeyRing(secretKeys, null); - } - - /** - * 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 {@link SecretKeyRingEditorInterface#done()}. - * - * @param secretKeys secret key ring - * @param referenceTime reference time used as signature creation date - * @return builder - */ - public static SecretKeyRingEditorInterface modifyKeyRing(PGPSecretKeyRing secretKeys, Date referenceTime) { - return new SecretKeyRingEditor(secretKeys, referenceTime); - } - - /** - * 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 #inspectKeyRing(PGPKeyRing, Date)} instead. - * - * @param keyRing key ring - * @return access object - */ - public static KeyRingInfo inspectKeyRing(PGPKeyRing keyRing) { - return new KeyRingInfo(keyRing); - } - - /** - * 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 at a specific time. - * - * @param keyRing key ring - * @param referenceTime date of inspection - * @return access object - */ - public static KeyRingInfo inspectKeyRing(PGPKeyRing keyRing, Date referenceTime) { - return new KeyRingInfo(keyRing, referenceTime); - } - - /** - * Access, and make changes to PGPainless policy on acceptable/default algorithms etc. - * - * @return policy - */ - public static Policy getPolicy() { - return Policy.getInstance(); - } - - /** - * Create different kinds of signatures on other keys. - * - * @return builder - */ - public static CertifyCertificate certify() { - return new CertifyCertificate(); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/AEADAlgorithm.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/AEADAlgorithm.java deleted file mode 100644 index a5885005..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/AEADAlgorithm.java +++ /dev/null @@ -1,100 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.algorithm; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.util.HashMap; -import java.util.Map; -import java.util.NoSuchElementException; - -/** - * List of AEAD algorithms defined in crypto-refresh-06. - * - * @see - * Crypto-Refresh-06 §9.6 - AEAD Algorithms - */ -public enum AEADAlgorithm { - - EAX(1, 16, 16), - OCB(2, 15, 16), - GCM(3, 12, 16), - ; - - private final int algorithmId; - private final int ivLength; - private final int tagLength; - - private static final Map MAP = new HashMap<>(); - - static { - for (AEADAlgorithm h : AEADAlgorithm.values()) { - MAP.put(h.algorithmId, h); - } - } - - AEADAlgorithm(int id, int ivLength, int tagLength) { - this.algorithmId = id; - this.ivLength = ivLength; - this.tagLength = tagLength; - } - - /** - * Return the ID of the AEAD algorithm. - * - * @return algorithm ID - */ - public int getAlgorithmId() { - return algorithmId; - } - - /** - * Return the length (in octets) of the IV. - * - * @return iv length - */ - public int getIvLength() { - return ivLength; - } - - /** - * Return the length (in octets) of the authentication tag. - * - * @return tag length - */ - public int getTagLength() { - return tagLength; - } - - /** - * Return the {@link AEADAlgorithm} 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 - */ - @Nullable - public static AEADAlgorithm fromId(int id) { - return MAP.get(id); - } - - /** - * Return the {@link AEADAlgorithm} value that corresponds to the provided algorithm id. - * If an invalid algorithm id was provided, throw a {@link NoSuchElementException}. - * - * @param id algorithm id - * @return enum value - * @throws NoSuchElementException in case of an unknown algorithm id - */ - @Nonnull - public static AEADAlgorithm requireFromId(int id) { - AEADAlgorithm algorithm = fromId(id); - if (algorithm == null) { - throw new NoSuchElementException("No AEADAlgorithm found for id " + id); - } - return algorithm; - } - -} 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 e155e367..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 final 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/CertificationType.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/CertificationType.java deleted file mode 100644 index f5c8ec7e..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/CertificationType.java +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.algorithm; - -import javax.annotation.Nonnull; - -/** - * Subset of {@link SignatureType}, reduced to certification types. - */ -public enum CertificationType { - - /** - * 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), - ; - - private final SignatureType signatureType; - - CertificationType(@Nonnull SignatureType signatureType) { - this.signatureType = signatureType; - } - - public @Nonnull SignatureType asSignatureType() { - return signatureType; - } -} 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 a2e78c5f..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/CompressionAlgorithm.java +++ /dev/null @@ -1,79 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.algorithm; - -import java.util.Map; -import java.util.NoSuchElementException; -import java.util.concurrent.ConcurrentHashMap; - -import org.bouncycastle.bcpg.CompressionAlgorithmTags; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * 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 - */ - @Nullable - public static CompressionAlgorithm fromId(int id) { - return MAP.get(id); - } - - /** - * Return the {@link CompressionAlgorithm} value that corresponds to the provided numerical id. - * If an invalid id is provided, thrown an {@link NoSuchElementException}. - * - * @param id id - * @return compression algorithm - * @throws NoSuchElementException in case of an unmapped id - */ - @Nonnull - public static CompressionAlgorithm requireFromId(int id) { - CompressionAlgorithm algorithm = fromId(id); - if (algorithm == null) { - throw new NoSuchElementException("No CompressionAlgorithm found for id " + id); - } - return algorithm; - } - - 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 5eda30c0..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. - * 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/java/org/pgpainless/algorithm/Feature.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/Feature.java deleted file mode 100644 index a0fc2974..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/Feature.java +++ /dev/null @@ -1,152 +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.NoSuchElementException; -import java.util.concurrent.ConcurrentHashMap; - -import org.bouncycastle.bcpg.sig.Features; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * 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 (version 1) 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!!! - * NOTE: This value is currently RESERVED. - * - * @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 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 - * Public-Key Packet Formats - */ - VERSION_5_PUBLIC_KEY(Features.FEATURE_VERSION_5_PUBLIC_KEY), - - /** - * Support for Symmetrically Encrypted Integrity Protected Data packet version 2. - * - * @see - * crypto-refresh-06 §5.13.2. Version 2 Sym. Encrypted Integrity Protected Data Packet Format - */ - MODIFICATION_DETECTION_2((byte) 0x08), - ; - - private static final Map MAP = new ConcurrentHashMap<>(); - - static { - for (Feature f : Feature.values()) { - MAP.put(f.featureId, f); - } - } - - /** - * Return the {@link Feature} encoded by the given id. - * If the id does not match any known features, return null. - * - * @param id feature id - * @return feature - */ - @Nullable - public static Feature fromId(byte id) { - return MAP.get(id); - } - - /** - * Return the {@link Feature} encoded by the given id. - * If the id does not match any known features, throw an {@link NoSuchElementException}. - * - * @param id feature id - * @return feature - * @throws NoSuchElementException if an unmatched feature id is encountered - */ - @Nonnull - public static Feature requireFromId(byte id) { - Feature feature = fromId(id); - if (feature == null) { - throw new NoSuchElementException("Unknown feature id encountered: " + id); - } - return feature; - } - - private final byte featureId; - - Feature(byte featureId) { - this.featureId = featureId; - } - - /** - * Return the id of the feature. - * - * @return feature id - */ - 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 - */ - @Nonnull - 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 0b9368bb..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/HashAlgorithm.java +++ /dev/null @@ -1,118 +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 java.util.NoSuchElementException; - -import org.bouncycastle.bcpg.HashAlgorithmTags; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * 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"), - SHA3_256 (12, "SHA3-256"), - SHA3_512 (14, "SHA3-512"), - ; - - 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 - */ - @Nullable - public static HashAlgorithm fromId(int id) { - return ID_MAP.get(id); - } - - /** - * Return the {@link HashAlgorithm} value that corresponds to the provided algorithm id. - * If an invalid algorithm id was provided, throw a {@link NoSuchElementException}. - * - * @param id algorithm id - * @return enum value - * @throws NoSuchElementException in case of an unknown algorithm id - */ - @Nonnull - public static HashAlgorithm requireFromId(int id) { - HashAlgorithm algorithm = fromId(id); - if (algorithm == null) { - throw new NoSuchElementException("No HashAlgorithm found for id " + id); - } - return algorithm; - } - - /** - * 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 - */ - @Nullable - public static HashAlgorithm fromName(String name) { - String algorithmName = name.toUpperCase(); - HashAlgorithm algorithm = NAME_MAP.get(algorithmName); - if (algorithm == null) { - algorithm = NAME_MAP.get(algorithmName.replace("-", "")); - } - return algorithm; - } - - 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 92205fd9..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/KeyFlag.java +++ /dev/null @@ -1,121 +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(); - } - - public static boolean containsAny(int mask, KeyFlag... flags) { - for (KeyFlag flag : flags) { - if (hasKeyFlag(mask, flag)) { - return true; - } - } - return false; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/OpenPgpPacket.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/OpenPgpPacket.java deleted file mode 100644 index 41e3fb08..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/OpenPgpPacket.java +++ /dev/null @@ -1,71 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.algorithm; - -import org.bouncycastle.bcpg.PacketTags; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.util.HashMap; -import java.util.Map; -import java.util.NoSuchElementException; - -public enum OpenPgpPacket { - PKESK(PacketTags.PUBLIC_KEY_ENC_SESSION), - SIG(PacketTags.SIGNATURE), - SKESK(PacketTags.SYMMETRIC_KEY_ENC_SESSION), - OPS(PacketTags.ONE_PASS_SIGNATURE), - SK(PacketTags.SECRET_KEY), - PK(PacketTags.PUBLIC_KEY), - SSK(PacketTags.SECRET_SUBKEY), - COMP(PacketTags.COMPRESSED_DATA), - SED(PacketTags.SYMMETRIC_KEY_ENC), - MARKER(PacketTags.MARKER), - LIT(PacketTags.LITERAL_DATA), - TRUST(PacketTags.TRUST), - UID(PacketTags.USER_ID), - PSK(PacketTags.PUBLIC_SUBKEY), - UATTR(PacketTags.USER_ATTRIBUTE), - SEIPD(PacketTags.SYM_ENC_INTEGRITY_PRO), - MDC(PacketTags.MOD_DETECTION_CODE), - - EXP_1(PacketTags.EXPERIMENTAL_1), - EXP_2(PacketTags.EXPERIMENTAL_2), - EXP_3(PacketTags.EXPERIMENTAL_3), - EXP_4(PacketTags.EXPERIMENTAL_4), - ; - - static final Map MAP = new HashMap<>(); - - static { - for (OpenPgpPacket p : OpenPgpPacket.values()) { - MAP.put(p.getTag(), p); - } - } - - final int tag; - - @Nullable - public static OpenPgpPacket fromTag(int tag) { - return MAP.get(tag); - } - - @Nonnull - public static OpenPgpPacket requireFromTag(int tag) { - OpenPgpPacket p = fromTag(tag); - if (p == null) { - throw new NoSuchElementException("No OpenPGP packet known for tag " + tag); - } - return p; - } - - OpenPgpPacket(int tag) { - this.tag = tag; - } - - int getTag() { - return tag; - } -} 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 c7599db9..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/PublicKeyAlgorithm.java +++ /dev/null @@ -1,163 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.algorithm; - -import java.util.Map; -import java.util.NoSuchElementException; -import java.util.concurrent.ConcurrentHashMap; - -import org.bouncycastle.bcpg.PublicKeyAlgorithmTags; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * 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 Deprecation notice - */ - @Deprecated - RSA_ENCRYPT (PublicKeyAlgorithmTags.RSA_ENCRYPT, false, true), - - /** - * RSA with usage of creating signatures. - * - * @deprecated see Deprecation notice - */ - @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 Deprecation notice - */ - @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 or null - */ - @Nullable - public static PublicKeyAlgorithm fromId(int id) { - return MAP.get(id); - } - - /** - * Return the {@link PublicKeyAlgorithm} that corresponds to the provided algorithm id. - * If an invalid id is provided, throw a {@link NoSuchElementException}. - * - * @param id numeric algorithm id - * @return algorithm - * @throws NoSuchElementException in case of an unmatched algorithm id - */ - @Nonnull - public static PublicKeyAlgorithm requireFromId(int id) { - PublicKeyAlgorithm algorithm = fromId(id); - if (algorithm == null) { - throw new NoSuchElementException("No PublicKeyAlgorithm found for id " + id); - } - return algorithm; - } - - 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 the algorithm can sign - */ - public boolean isSigningCapable() { - return signingCapable; - } - - /** - * Return true if this public key algorithm can be used as an encryption algorithm. - * - * @return true if the algorithm can encrypt - */ - public boolean isEncryptionCapable() { - return encryptionCapable; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/RevocationState.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/RevocationState.java deleted file mode 100644 index 8e4a60d3..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/RevocationState.java +++ /dev/null @@ -1,131 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.algorithm; - -import org.pgpainless.util.DateUtil; - -import javax.annotation.Nonnull; -import java.util.Date; -import java.util.NoSuchElementException; - -public final class RevocationState implements Comparable { - - private final RevocationStateType type; - private final Date date; - - private RevocationState(RevocationStateType type) { - this(type, null); - } - - private RevocationState(RevocationStateType type, Date date) { - this.type = type; - if (type == RevocationStateType.softRevoked && date == null) { - throw new NullPointerException("If type is 'softRevoked' then date cannot be null."); - } - this.date = date; - } - - public static RevocationState notRevoked() { - return new RevocationState(RevocationStateType.notRevoked); - } - - public static RevocationState softRevoked(@Nonnull Date date) { - return new RevocationState(RevocationStateType.softRevoked, date); - } - - public static RevocationState hardRevoked() { - return new RevocationState(RevocationStateType.hardRevoked); - } - - public RevocationStateType getType() { - return type; - } - - public @Nonnull Date getDate() { - if (!isSoftRevocation()) { - throw new NoSuchElementException("RevocationStateType is not equal to 'softRevoked'. Cannot extract date."); - } - return date; - } - - public boolean isHardRevocation() { - return getType() == RevocationStateType.hardRevoked; - } - - public boolean isSoftRevocation() { - return getType() == RevocationStateType.softRevoked; - } - - public boolean isNotRevoked() { - return getType() == RevocationStateType.notRevoked; - } - - @Override - public String toString() { - String out = getType().toString(); - if (isSoftRevocation()) { - out = out + " (" + DateUtil.formatUTCDate(date) + ")"; - } - return out; - } - - @Override - public int compareTo(@Nonnull RevocationState o) { - switch (getType()) { - case notRevoked: - if (o.isNotRevoked()) { - return 0; - } else { - return -1; - } - - case softRevoked: - if (o.isNotRevoked()) { - return 1; - } else if (o.isSoftRevocation()) { - // Compare soft dates in reverse - return o.getDate().compareTo(getDate()); - } else { - return -1; - } - - case hardRevoked: - if (o.isHardRevocation()) { - return 0; - } else { - return 1; - } - - default: - throw new AssertionError("Unknown type: " + type); - } - } - - @Override - public int hashCode() { - return type.hashCode() * 31 + (isSoftRevocation() ? getDate().hashCode() : 0); - } - - @Override - public boolean equals(Object obj) { - if (obj == null) { - return false; - } - if (this == obj) { - return true; - } - if (!(obj instanceof RevocationState)) { - return false; - } - RevocationState other = (RevocationState) obj; - if (getType() != other.getType()) { - return false; - } - if (isSoftRevocation()) { - return DateUtil.toSecondsPrecision(getDate()).getTime() == DateUtil.toSecondsPrecision(other.getDate()).getTime(); - } - return true; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/RevocationStateType.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/RevocationStateType.java deleted file mode 100644 index d1757255..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/RevocationStateType.java +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.algorithm; - -public enum 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/java/org/pgpainless/algorithm/SignatureSubpacket.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/SignatureSubpacket.java deleted file mode 100644 index 9429f0c6..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/SignatureSubpacket.java +++ /dev/null @@ -1,462 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.algorithm; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -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.NoSuchElementException; -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 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 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 - */ - 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 - * 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 - */ - preferredHashAlgorithms(PREFERRED_HASH_ALGS), - - /** - * 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 - */ - preferredCompressionAlgorithms(PREFERRED_COMP_ALGS), - - /** - * 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 - */ - keyServerPreferences(KEY_SERVER_PREFS), - - /** - * 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 - */ - 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 - * 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 - */ - 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 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. - * If an unmatched code is presented, return null. - * - * @param code id - * @return signature subpacket - */ - @Nullable - public static SignatureSubpacket fromCode(int code) { - return MAP.get(code); - } - - /** - * Return the {@link SignatureSubpacket} that corresponds to the provided code. - * - * @param code code - * @return signature subpacket - * @throws NoSuchElementException in case of an unmatched subpacket tag - */ - @Nonnull - public static SignatureSubpacket requireFromCode(int code) { - SignatureSubpacket tag = fromCode(code); - if (tag == null) { - throw new NoSuchElementException("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) { - try { - tags.add(requireFromCode(code)); - } catch (NoSuchElementException e) { - // skip - } - } - 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 c2f02989..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/SignatureType.java +++ /dev/null @@ -1,225 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.algorithm; - -import org.bouncycastle.openpgp.PGPSignature; - -import javax.annotation.Nonnull; -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 - * @throws IllegalArgumentException in case of an unmatched signature type code - */ - @Nonnull - 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 b0617bbb..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/StreamEncoding.java +++ /dev/null @@ -1,101 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.algorithm; - -import java.util.Map; -import java.util.NoSuchElementException; -import java.util.concurrent.ConcurrentHashMap; - -import org.bouncycastle.openpgp.PGPLiteralData; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * 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. - * If no matching encoding is found, return null. - * - * @param code identifier - * @return encoding enum - */ - @Nullable - public static StreamEncoding fromCode(int code) { - return MAP.get((char) code); - } - - /** - * Return the {@link StreamEncoding} corresponding to the provided code identifier. - * If no matching encoding is found, throw a {@link NoSuchElementException}. - * - * @param code identifier - * @return encoding enum - * - * @throws NoSuchElementException in case of an unmatched identifier - */ - @Nonnull - public static StreamEncoding requireFromCode(int code) { - StreamEncoding encoding = fromCode(code); - if (encoding == null) { - throw new NoSuchElementException("No StreamEncoding found for code " + code); - } - return encoding; - } -} 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 e04f21a5..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/SymmetricKeyAlgorithm.java +++ /dev/null @@ -1,150 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.algorithm; - -import java.util.Map; -import java.util.NoSuchElementException; -import java.util.concurrent.ConcurrentHashMap; - -import org.bouncycastle.bcpg.SymmetricKeyAlgorithmTags; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * 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 - */ - @Nullable - public static SymmetricKeyAlgorithm fromId(int id) { - return MAP.get(id); - } - - /** - * Return the {@link SymmetricKeyAlgorithm} enum that corresponds to the provided numeric id. - * If an invalid id is provided, throw a {@link NoSuchElementException}. - * - * @param id numeric algorithm id - * @return symmetric key algorithm enum - * - * @throws NoSuchElementException if an unmatched id is provided - */ - @Nonnull - public static SymmetricKeyAlgorithm requireFromId(int id) { - SymmetricKeyAlgorithm algorithm = fromId(id); - if (algorithm == null) { - throw new NoSuchElementException("No SymmetricKeyAlgorithm found for id " + id); - } - return algorithm; - } - - 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/Trustworthiness.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/Trustworthiness.java deleted file mode 100644 index 26e66e9c..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/Trustworthiness.java +++ /dev/null @@ -1,188 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.algorithm; - -/** - * Facade class for {@link 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. - */ -public class Trustworthiness { - - private final int amount; - private final int depth; - - public static final int THRESHOLD_FULLY_CONVINCED = 120; // greater or equal is fully trusted - public static final int MARGINALLY_CONVINCED = 60; // default value for marginally convinced - public static final int NOT_TRUSTED = 0; // 0 is not trusted - - public Trustworthiness(int amount, int depth) { - this.amount = capAmount(amount); - this.depth = capDepth(depth); - } - - /** - * Get the trust amount. - * This value means how confident the issuer of the signature is in validity of the binding. - * - * @return trust amount - */ - public int getAmount() { - return amount; - } - - /** - * Get the depth of the trust signature. - * This value controls, whether the certificate can act as a trusted introducer. - * - * @return depth - */ - public int getDepth() { - return depth; - } - - /** - * Returns true, if the trust amount is equal to 0. - * This means the key is not trusted. - * - * Otherwise return false - * @return true if untrusted - */ - public boolean isNotTrusted() { - return getAmount() == 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 - */ - public boolean isMarginallyTrusted() { - return getAmount() > 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 - */ - public boolean isFullyTrusted() { - return getAmount() >= 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 - */ - public boolean isIntroducer() { - return getDepth() >= 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 - */ - public boolean canIntroduce(int otherDepth) { - return getDepth() > 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 - */ - public boolean canIntroduce(Trustworthiness other) { - return canIntroduce(other.getDepth()); - } - - /** - * This means that we are fully convinced of the trustworthiness of the key. - * - * @return builder - */ - public static Builder fullyTrusted() { - return new Builder(THRESHOLD_FULLY_CONVINCED); - } - - /** - * This means that we are marginally (partially) convinced of the trustworthiness of the key. - * - * @return builder - */ - public static Builder marginallyTrusted() { - return new Builder(MARGINALLY_CONVINCED); - } - - /** - * This means that we do not trust the key. - * Can be used to overwrite previous trust. - * - * @return builder - */ - public static Builder untrusted() { - return new Builder(NOT_TRUSTED); - } - - public static final class Builder { - - private final int amount; - - private Builder(int amount) { - this.amount = amount; - } - - /** - * The key is a trusted introducer (depth 1). - * Certifications made by this key are considered trustworthy. - * - * @return trust - */ - public Trustworthiness introducer() { - return new Trustworthiness(amount, 1); - } - - /** - * The key is a meta introducer (depth 2). - * This key can introduce trusted introducers of depth 1. - * - * @return trust - */ - public Trustworthiness metaIntroducer() { - return new 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 - */ - public Trustworthiness metaIntroducerOfDepth(int n) { - return new Trustworthiness(amount, n); - } - } - - private static int capAmount(int amount) { - if (amount < 0 || amount > 255) { - throw new IllegalArgumentException("Trust amount MUST be a value between 0 and 255"); - } - return amount; - } - - private static int capDepth(int depth) { - if (depth < 0 || depth > 255) { - throw new IllegalArgumentException("Trust depth MUST be a value between 0 and 255"); - } - return depth; - } - -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/negotiation/HashAlgorithmNegotiator.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/negotiation/HashAlgorithmNegotiator.java deleted file mode 100644 index 18fe53f9..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/negotiation/HashAlgorithmNegotiator.java +++ /dev/null @@ -1,70 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.algorithm.negotiation; - -import java.util.Set; - -import org.pgpainless.algorithm.HashAlgorithm; -import org.pgpainless.policy.Policy; - -/** - * Interface for a class that negotiates {@link HashAlgorithm HashAlgorithms}. - * - * You can provide your own implementation using custom logic by implementing the - * {@link #negotiateHashAlgorithm(Set)} method. - */ -public interface HashAlgorithmNegotiator { - - /** - * Pick one {@link HashAlgorithm} from the ordered set of acceptable algorithms. - * - * @param orderedHashAlgorithmPreferencesSet hash algorithm preferences - * @return picked algorithms - */ - HashAlgorithm negotiateHashAlgorithm(Set orderedHashAlgorithmPreferencesSet); - - /** - * Return an instance that negotiates {@link HashAlgorithm HashAlgorithms} used for non-revocation signatures - * based on the given {@link Policy}. - * - * @param policy algorithm policy - * @return negotiator - */ - static HashAlgorithmNegotiator negotiateSignatureHashAlgorithm(Policy policy) { - return negotiateByPolicy(policy.getSignatureHashAlgorithmPolicy()); - } - - /** - * Return an instance that negotiates {@link HashAlgorithm HashAlgorithms} used for revocation signatures - * based on the given {@link Policy}. - * - * @param policy algorithm policy - * @return negotiator - */ - static HashAlgorithmNegotiator negotiateRevocationSignatureAlgorithm(Policy policy) { - return negotiateByPolicy(policy.getRevocationSignatureHashAlgorithmPolicy()); - } - - /** - * Return an instance that negotiates {@link HashAlgorithm HashAlgorithms} based on the given - * {@link Policy.HashAlgorithmPolicy}. - * - * @param hashAlgorithmPolicy algorithm policy for hash algorithms - * @return negotiator - */ - static HashAlgorithmNegotiator negotiateByPolicy(Policy.HashAlgorithmPolicy hashAlgorithmPolicy) { - return new HashAlgorithmNegotiator() { - @Override - public HashAlgorithm negotiateHashAlgorithm(Set orderedPreferencesSet) { - for (HashAlgorithm preference : orderedPreferencesSet) { - if (hashAlgorithmPolicy.isAcceptable(preference)) { - return preference; - } - } - return hashAlgorithmPolicy.defaultHashAlgorithm(); - } - }; - } -} 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 de8ced24..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/negotiation/SymmetricKeyAlgorithmNegotiator.java +++ /dev/null @@ -1,105 +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. - * 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 - */ - SymmetricKeyAlgorithm negotiate( - Policy.SymmetricKeyAlgorithmPolicy policy, - SymmetricKeyAlgorithm override, - List> keyPreferences); - - /** - * Return an instance that negotiates a {@link SymmetricKeyAlgorithm} by selecting the most popular acceptable - * algorithm from the list of preferences. - * - * This negotiator has the best chances to select an algorithm which is understood by all recipients. - * - * @return negotiator that selects by popularity - */ - 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/ConsumerOptions.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java deleted file mode 100644 index d0d9230b..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java +++ /dev/null @@ -1,508 +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.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; -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. - */ -public class ConsumerOptions { - - private boolean ignoreMDCErrors = false; - private boolean forceNonOpenPgpData = false; - - private Date verifyNotBefore = null; - private Date verifyNotAfter = new Date(); - - private final CertificateSource certificates = new CertificateSource(); - private final Set detachedSignatures = new HashSet<>(); - private MissingPublicKeyCallback missingCertificateCallback = null; - - // Session key for decryption without passphrase/key - private SessionKey sessionKey = null; - private final Map customPublicKeyDataDecryptorFactories = - new HashMap<>(); - - private final Map decryptionKeys = new HashMap<>(); - private final Set decryptionPassphrases = new HashSet<>(); - private MissingKeyPassphraseStrategy missingKeyPassphraseStrategy = MissingKeyPassphraseStrategy.INTERACTIVE; - - private MultiPassStrategy multiPassStrategy = new InMemoryMultiPassStrategy(); - - public static ConsumerOptions get() { - return new ConsumerOptions(); - } - - /** - * 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.addCertificate(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; - } - - /** - * Add some detached signatures from the given {@link 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 - */ - public ConsumerOptions addVerificationOfDetachedSignatures(InputStream signatureInputStream) - throws IOException, PGPException { - List signatures = SignatureUtils.readSignatures(signatureInputStream); - return addVerificationOfDetachedSignatures(signatures); - } - - /** - * Add some detached signatures for verification. - * - * @param detachedSignatures detached signatures - * @return options - */ - 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. - * - * @see RFC4880 on Session Keys - * - * @param sessionKey session key - * @return options - */ - public ConsumerOptions setSessionKey(@Nonnull SessionKey sessionKey) { - this.sessionKey = sessionKey; - return this; - } - - /** - * Return the session key. - * - * @return session key or null - */ - public @Nullable SessionKey getSessionKey() { - return sessionKey; - } - - /** - * 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; - } - - /** - * Add a custom {@link PublicKeyDataDecryptorFactory} which enable decryption of messages, e.g. using - * hardware-backed secret keys. - * (See e.g. {@link org.pgpainless.decryption_verification.HardwareSecurity.HardwareDataDecryptorFactory}). - * - * @param factory decryptor factory - * @return options - */ - public ConsumerOptions addCustomDecryptorFactory(@Nonnull CustomPublicKeyDataDecryptorFactory factory) { - this.customPublicKeyDataDecryptorFactories.put(factory.getSubkeyIdentifier(), factory); - return this; - } - - /** - * Return the custom {@link PublicKeyDataDecryptorFactory PublicKeyDataDecryptorFactories} that were - * set by the user. - * These factories can be used to decrypt session keys using a custom logic. - * - * @return custom decryptor factories - */ - Map getCustomDecryptorFactories() { - return new HashMap<>(customPublicKeyDataDecryptorFactories); - } - - /** - * Return the set of available decryption keys. - * - * @return decryption keys - */ - public @Nonnull Set getDecryptionKeys() { - return Collections.unmodifiableSet(decryptionKeys.keySet()); - } - - /** - * Return the set of available message decryption passphrases. - * - * @return decryption passphrases - */ - public @Nonnull Set getDecryptionPassphrases() { - return Collections.unmodifiableSet(decryptionPassphrases); - } - - /** - * Return the explicitly set verification certificates. - * - * @deprecated use {@link #getCertificateSource()} instead. - * @return verification certs - */ - @Deprecated - public @Nonnull Set getCertificates() { - return certificates.getExplicitCertificates(); - } - - /** - * Return an object holding available certificates for signature verification. - * - * @return certificate source - */ - public @Nonnull CertificateSource getCertificateSource() { - return 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 - */ - public @Nullable MissingPublicKeyCallback getMissingCertificateCallback() { - return missingCertificateCallback; - } - - /** - * Return the {@link SecretKeyRingProtector} for the given {@link PGPSecretKeyRing}. - * - * @param decryptionKeyRing secret key - * @return protector for that particular secret key - */ - public @Nonnull SecretKeyRingProtector getSecretKeyProtector(PGPSecretKeyRing decryptionKeyRing) { - return decryptionKeys.get(decryptionKeyRing); - } - - /** - * Return the set of detached signatures the user provided. - * - * @return detached signatures - */ - 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; - } - - /** - * Force PGPainless to handle the data provided by the {@link InputStream} as non-OpenPGP data. - * This workaround might come in handy if PGPainless accidentally mistakes the data for binary OpenPGP data. - * - * @return options - */ - public ConsumerOptions forceNonOpenPgpData() { - this.forceNonOpenPgpData = true; - return this; - } - - /** - * Return true, if the ciphertext should be handled as binary non-OpenPGP data. - * - * @return true if non-OpenPGP data is forced - */ - boolean isForceNonOpenPgpData() { - return forceNonOpenPgpData; - } - - /** - * 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; - } - - /** - * Set a custom multi-pass strategy for processing cleartext-signed messages. - * Uses {@link InMemoryMultiPassStrategy} by default. - * - * @param multiPassStrategy multi-pass caching strategy - * @return builder - */ - public ConsumerOptions setMultiPassStrategy(@Nonnull MultiPassStrategy multiPassStrategy) { - this.multiPassStrategy = multiPassStrategy; - return this; - } - - /** - * Return the currently configured {@link MultiPassStrategy}. - * Defaults to {@link InMemoryMultiPassStrategy}. - * - * @return multi-pass strategy - */ - public MultiPassStrategy getMultiPassStrategy() { - return multiPassStrategy; - } - - /** - * Source for OpenPGP certificates. - * When verifying signatures on a message, this object holds available signer certificates. - */ - public static class CertificateSource { - - private Set explicitCertificates = new HashSet<>(); - - /** - * Add a certificate as verification cert explicitly. - * - * @param certificate certificate - */ - public void addCertificate(PGPPublicKeyRing certificate) { - this.explicitCertificates.add(certificate); - } - - /** - * Return the set of explicitly set verification certificates. - * @return explicitly set verification certs - */ - public Set getExplicitCertificates() { - return Collections.unmodifiableSet(explicitCertificates); - } - - /** - * 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 - */ - public PGPPublicKeyRing getCertificate(long keyId) { - - for (PGPPublicKeyRing cert : explicitCertificates) { - if (cert.getPublicKey(keyId) != null) { - return cert; - } - } - - return null; - } - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactory.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactory.java deleted file mode 100644 index 91902cc7..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactory.java +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification; - -import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; -import org.pgpainless.key.SubkeyIdentifier; - -/** - * Custom {@link 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(CustomPublicKeyDataDecryptorFactory) - */ -public interface CustomPublicKeyDataDecryptorFactory extends PublicKeyDataDecryptorFactory { - - /** - * Return the {@link SubkeyIdentifier} for which this particular {@link CustomPublicKeyDataDecryptorFactory} - * is intended. - * - * @return subkey identifier - */ - SubkeyIdentifier getSubkeyIdentifier(); - -} 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 96b4ad60..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilder.java +++ /dev/null @@ -1,42 +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; - -/** - * Builder class that takes an {@link InputStream} of ciphertext (or plaintext signed data) - * and combines it with a configured {@link ConsumerOptions} object to form a {@link DecryptionStream} which - * can be used to decrypt an OpenPGP message or verify signatures. - */ -public class DecryptionBuilder implements DecryptionBuilderInterface { - - @Override - public DecryptWith onInputStream(@Nonnull InputStream inputStream) { - return new DecryptWithImpl(inputStream); - } - - static class DecryptWithImpl implements DecryptWith { - - private final InputStream inputStream; - - DecryptWithImpl(InputStream inputStream) { - this.inputStream = inputStream; - } - - @Override - public DecryptionStream withOptions(ConsumerOptions consumerOptions) throws PGPException, IOException { - if (consumerOptions == null) { - throw new IllegalArgumentException("Consumer options cannot be null."); - } - - return OpenPgpMessageInputStream.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 28642bbf..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStream.java +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification; - -import java.io.InputStream; - -/** - * Abstract definition of an {@link InputStream} which can be used to decrypt / verify OpenPGP messages. - */ -public abstract class DecryptionStream extends InputStream { - - /** - * Return {@link MessageMetadata metadata} about the decrypted / verified message. - * The {@link DecryptionStream} MUST be closed via {@link #close()} before the metadata object can be accessed. - * - * @return message metadata - */ - public abstract MessageMetadata getMetadata(); - - /** - * Return a {@link OpenPgpMetadata} object containing information about the decrypted / verified message. - * The {@link DecryptionStream} MUST be closed via {@link #close()} before the metadata object can be accessed. - * - * @return message metadata - * @deprecated use {@link #getMetadata()} instead. - */ - @Deprecated - public OpenPgpMetadata getResult() { - return getMetadata().toLegacyMetadata(); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java deleted file mode 100644 index cdadf2c4..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java +++ /dev/null @@ -1,96 +0,0 @@ -// 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.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. - */ -public class HardwareSecurity { - - public 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 {@link HardwareSecurityException} is thrown. - * - * @param keyId id of the key - * @param keyAlgorithm algorithm - * @param sessionKeyData encrypted session key - * - * @return decrypted session key - * @throws HardwareSecurityException exception - */ - byte[] decryptSessionKey(long keyId, int keyAlgorithm, byte[] sessionKeyData) - throws HardwareSecurityException; - - } - - /** - * Implementation of {@link PublicKeyDataDecryptorFactory} which delegates decryption of encrypted session keys - * to a {@link DecryptionCallback}. - * Users can provide such a callback to delegate decryption of messages to hardware security SDKs. - */ - public static class HardwareDataDecryptorFactory implements CustomPublicKeyDataDecryptorFactory { - - private final DecryptionCallback callback; - // luckily we can instantiate the BcPublicKeyDataDecryptorFactory with null as argument. - private final PublicKeyDataDecryptorFactory factory = - new BcPublicKeyDataDecryptorFactory(null); - private final SubkeyIdentifier subkey; - - /** - * Create a new {@link HardwareDataDecryptorFactory}. - * - * @param subkeyIdentifier identifier of the decryption subkey - * @param callback decryption callback - */ - public HardwareDataDecryptorFactory(SubkeyIdentifier subkeyIdentifier, DecryptionCallback callback) { - this.callback = callback; - this.subkey = subkeyIdentifier; - } - - @Override - public byte[] recoverSessionData(int keyAlgorithm, byte[][] secKeyData) - throws PGPException { - try { - // delegate decryption to the callback - return callback.decryptSessionKey(subkey.getSubkeyId(), keyAlgorithm, secKeyData[0]); - } catch (HardwareSecurityException e) { - throw new PGPException("Hardware-backed decryption failed.", e); - } - } - - @Override - public PGPDataDecryptor createDataDecryptor(boolean withIntegrityPacket, int encAlgorithm, byte[] key) - throws PGPException { - return factory.createDataDecryptor(withIntegrityPacket, encAlgorithm, key); - } - - @Override - public PGPDataDecryptor createDataDecryptor(int aeadAlgorithm, byte[] iv, int chunkSize, int encAlgorithm, byte[] key) - throws PGPException { - return factory.createDataDecryptor(aeadAlgorithm, iv, chunkSize, encAlgorithm, key); - } - - @Override - public SubkeyIdentifier getSubkeyIdentifier() { - return subkey; - } - } - - public static class HardwareSecurityException - extends Exception { - - } - -} 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 37dcfca4..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/IntegrityProtectedInputStream.java +++ /dev/null @@ -1,60 +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; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class IntegrityProtectedInputStream extends InputStream { - - private static final Logger LOGGER = LoggerFactory.getLogger(IntegrityProtectedInputStream.class); - - private final InputStream inputStream; - private final PGPEncryptedData encryptedData; - private final ConsumerOptions options; - private boolean closed = false; - - 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 (closed) { - return; - } - closed = true; - - if (encryptedData.isIntegrityProtected() && !options.isIgnoreMDCErrors()) { - try { - if (!encryptedData.verify()) { - throw new ModificationDetectionException(); - } - LOGGER.debug("Integrity Protection check passed"); - } catch (PGPException e) { - throw new IOException("Data appears to not be integrity protected.", 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 3dda0f5d..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageInspector.java +++ /dev/null @@ -1,146 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.Charset; -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.bouncycastle.openpgp.PGPUtil; -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. - * - * @param message OpenPGP message - * @return encryption info - * - * @throws PGPException in case the message is broken - * @throws IOException in case of an IO error - */ - public static EncryptionInfo determineEncryptionInfoForMessage(String message) throws PGPException, IOException { - @SuppressWarnings("CharsetObjectCanBeUsed") - Charset charset = Charset.forName("UTF-8"); - return determineEncryptionInfoForMessage(new ByteArrayInputStream(message.getBytes(charset))); - } - - /** - * 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 in case of an IO error - * @throws PGPException if the message is broken - */ - 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, IOException { - PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(dataIn); - - Object next; - while ((next = objectFactory.nextObject()) != null) { - 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; - } - } - // Data is encrypted, we cannot go deeper - return; - } - - if (next instanceof PGPCompressedData) { - PGPCompressedData compressed = (PGPCompressedData) next; - InputStream decompressed = compressed.getDataStream(); - InputStream decoded = PGPUtil.getDecoderStream(decompressed); - objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(decoded); - } - - if (next instanceof PGPLiteralData) { - return; - } - } - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java deleted file mode 100644 index 648b99ab..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java +++ /dev/null @@ -1,808 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification; - -import java.util.ArrayList; -import java.util.Date; -import java.util.Iterator; -import java.util.List; -import java.util.NoSuchElementException; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import org.bouncycastle.openpgp.PGPKeyRing; -import org.bouncycastle.openpgp.PGPLiteralData; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.pgpainless.algorithm.CompressionAlgorithm; -import org.pgpainless.algorithm.StreamEncoding; -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; -import org.pgpainless.exception.MalformedOpenPgpMessageException; -import org.pgpainless.key.SubkeyIdentifier; -import org.pgpainless.util.SessionKey; - -/** - * View for extracting metadata about a {@link Message}. - */ -public class MessageMetadata { - - protected Message message; - - public MessageMetadata(@Nonnull Message message) { - this.message = message; - } - - /** - * Convert this {@link MessageMetadata} object into a legacy {@link OpenPgpMetadata} object. - * This method is intended to be used for a transition period between the 1.3 / 1.4+ branches. - * TODO: Remove in 1.5.X - * - * @return converted {@link OpenPgpMetadata} object - */ - public @Nonnull OpenPgpMetadata toLegacyMetadata() { - OpenPgpMetadata.Builder resultBuilder = OpenPgpMetadata.getBuilder(); - resultBuilder.setCompressionAlgorithm(getCompressionAlgorithm()); - resultBuilder.setModificationDate(getModificationDate()); - resultBuilder.setFileName(getFilename()); - resultBuilder.setFileEncoding(getLiteralDataEncoding()); - resultBuilder.setSessionKey(getSessionKey()); - resultBuilder.setDecryptionKey(getDecryptionKey()); - - for (SignatureVerification accepted : getVerifiedDetachedSignatures()) { - resultBuilder.addVerifiedDetachedSignature(accepted); - } - for (SignatureVerification.Failure rejected : getRejectedDetachedSignatures()) { - resultBuilder.addInvalidDetachedSignature(rejected.getSignatureVerification(), rejected.getValidationException()); - } - - for (SignatureVerification accepted : getVerifiedInlineSignatures()) { - resultBuilder.addVerifiedInbandSignature(accepted); - } - for (SignatureVerification.Failure rejected : getRejectedInlineSignatures()) { - resultBuilder.addInvalidInbandSignature(rejected.getSignatureVerification(), rejected.getValidationException()); - } - if (message.isCleartextSigned()) { - resultBuilder.setCleartextSigned(); - } - - return resultBuilder.build(); - } - - public boolean isUsingCleartextSignatureFramework() { - return message.isCleartextSigned(); - } - - public boolean isEncrypted() { - SymmetricKeyAlgorithm algorithm = getEncryptionAlgorithm(); - return algorithm != null && algorithm != SymmetricKeyAlgorithm.NULL; - } - - public boolean isEncryptedFor(@Nonnull PGPKeyRing keys) { - Iterator encryptionLayers = getEncryptionLayers(); - while (encryptionLayers.hasNext()) { - EncryptedData encryptedData = encryptionLayers.next(); - for (long recipient : encryptedData.getRecipients()) { - PGPPublicKey key = keys.getPublicKey(recipient); - if (key != null) { - return true; - } - } - } - return false; - } - - public @Nonnull Iterator getEncryptionLayers() { - return new LayerIterator(message) { - @Override - public boolean matches(Packet layer) { - return layer instanceof EncryptedData; - } - - @Override - public EncryptedData getProperty(Layer last) { - return (EncryptedData) last; - } - }; - } - - /** - * Return the {@link SymmetricKeyAlgorithm} of the outermost encrypted data packet, or null if message is - * unencrypted. - * - * @return encryption algorithm - */ - public @Nullable SymmetricKeyAlgorithm getEncryptionAlgorithm() { - return firstOrNull(getEncryptionAlgorithms()); - } - - /** - * Return an {@link Iterator} of {@link SymmetricKeyAlgorithm SymmetricKeyAlgorithms} 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. - * - * @return iterator of symmetric encryption algorithms - */ - public @Nonnull Iterator getEncryptionAlgorithms() { - return map(getEncryptionLayers(), encryptedData -> encryptedData.algorithm); - } - - public @Nonnull Iterator getCompressionLayers() { - return new LayerIterator(message) { - @Override - boolean matches(Packet layer) { - return layer instanceof CompressedData; - } - - @Override - CompressedData getProperty(Layer last) { - return (CompressedData) last; - } - }; - } - - /** - * Return the {@link CompressionAlgorithm} of the outermost compressed data packet, or null, if the message - * does not contain any compressed data packets. - * - * @return compression algorithm - */ - public @Nullable CompressionAlgorithm getCompressionAlgorithm() { - return firstOrNull(getCompressionAlgorithms()); - } - - /** - * Return an {@link Iterator} of {@link CompressionAlgorithm CompressionAlgorithms} 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. - * - * @return iterator of compression algorithms - */ - public @Nonnull Iterator getCompressionAlgorithms() { - return map(getCompressionLayers(), compressionLayer -> compressionLayer.algorithm); - } - - /** - * Return the {@link SessionKey} of the outermost encrypted data packet. - * If the message was unencrypted, this method returns
null
. - * - * @return session key of the message - */ - public @Nullable SessionKey getSessionKey() { - return firstOrNull(getSessionKeys()); - } - - /** - * Return an {@link Iterator} of {@link SessionKey SessionKeys} 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. - * - * @return iterator of session keys - */ - public @Nonnull Iterator getSessionKeys() { - return map(getEncryptionLayers(), encryptedData -> encryptedData.sessionKey); - } - - public boolean isVerifiedSignedBy(@Nonnull PGPKeyRing keys) { - return isVerifiedInlineSignedBy(keys) || isVerifiedDetachedSignedBy(keys); - } - - public List getVerifiedSignatures() { - List allVerifiedSignatures = getVerifiedInlineSignatures(); - allVerifiedSignatures.addAll(getVerifiedDetachedSignatures()); - return allVerifiedSignatures; - } - - public boolean isVerifiedDetachedSignedBy(@Nonnull PGPKeyRing keys) { - return containsSignatureBy(getVerifiedDetachedSignatures(), keys); - } - - /** - * Return a list of all verified detached signatures. - * This list contains all acceptable, correct detached signatures. - * - * @return verified detached signatures - */ - public @Nonnull List getVerifiedDetachedSignatures() { - return message.getVerifiedDetachedSignatures(); - } - - /** - * Return a list of all rejected detached signatures. - * - * @return rejected detached signatures - */ - public @Nonnull List getRejectedDetachedSignatures() { - return message.getRejectedDetachedSignatures(); - } - - public boolean isVerifiedInlineSignedBy(@Nonnull PGPKeyRing keys) { - return containsSignatureBy(getVerifiedInlineSignatures(), keys); - } - - /** - * Return a list of all verified inline-signatures. - * This list contains all acceptable, correct signatures that were part of the message itself. - * - * @return verified inline signatures - */ - public @Nonnull List getVerifiedInlineSignatures() { - List verifications = new ArrayList<>(); - Iterator> verificationsByLayer = getVerifiedInlineSignaturesByLayer(); - while (verificationsByLayer.hasNext()) { - verifications.addAll(verificationsByLayer.next()); - } - return verifications; - } - - /** - * Return an {@link Iterator} of {@link List Lists} of verified inline-signatures of the message. - * 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 {@link 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. - * - * @return iterator of lists of signatures by-layer. - */ - public @Nonnull Iterator> getVerifiedInlineSignaturesByLayer() { - return new LayerIterator>(message) { - @Override - boolean matches(Packet layer) { - return layer instanceof Layer; - } - - @Override - List getProperty(Layer last) { - List list = new ArrayList<>(); - list.addAll(last.getVerifiedOnePassSignatures()); - list.addAll(last.getVerifiedPrependedSignatures()); - return list; - } - }; - } - - /** - * Return a list of all rejected inline-signatures of the message. - * - * @return list of rejected inline-signatures - */ - public @Nonnull List getRejectedInlineSignatures() { - List rejected = new ArrayList<>(); - Iterator> rejectedByLayer = getRejectedInlineSignaturesByLayer(); - while (rejectedByLayer.hasNext()) { - rejected.addAll(rejectedByLayer.next()); - } - return rejected; - } - - /** - * Similar to {@link #getVerifiedInlineSignaturesByLayer()}, this method returns all rejected inline-signatures - * of the message, but organized by layer. - * - * @return rejected inline-signatures by-layer - */ - public @Nonnull Iterator> getRejectedInlineSignaturesByLayer() { - return new LayerIterator>(message) { - @Override - boolean matches(Packet layer) { - return layer instanceof Layer; - } - - @Override - List getProperty(Layer last) { - List list = new ArrayList<>(); - list.addAll(last.getRejectedOnePassSignatures()); - list.addAll(last.getRejectedPrependedSignatures()); - return list; - } - }; - } - - private static boolean containsSignatureBy(@Nonnull List verifications, - @Nonnull PGPKeyRing keys) { - for (SignatureVerification verification : verifications) { - SubkeyIdentifier issuer = verification.getSigningKey(); - if (issuer == null) { - // No issuer, shouldn't happen, but better be safe and skip... - continue; - } - - if (keys.getPublicKey().getKeyID() != issuer.getPrimaryKeyId()) { - // Wrong cert - continue; - } - - if (keys.getPublicKey(issuer.getSubkeyId()) != null) { - // Matching cert and signing key - return true; - } - } - return false; - } - - /** - * Return the 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. - * - * @return filename - * @see RFC4880 §5.9. Literal Data Packet - */ - public @Nullable String getFilename() { - LiteralData literalData = findLiteralData(); - if (literalData == null) { - return null; - } - return literalData.getFileName(); - } - - /** - * Returns true, if the filename of the literal data packet indicates that the data is intended for your eyes only. - * - * @return isForYourEyesOnly - */ - public boolean isForYourEyesOnly() { - return PGPLiteralData.CONSOLE.equals(getFilename()); - } - - /** - * Return the 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. - * - * @return modification date - * @see RFC4880 §5.9. Literal Data Packet - */ - public @Nullable Date getModificationDate() { - LiteralData literalData = findLiteralData(); - if (literalData == null) { - return null; - } - return literalData.getModificationDate(); - } - - /** - * Return the 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. - * - * @return format - * @see RFC4880 §5.9. Literal Data Packet - */ - public @Nullable StreamEncoding getLiteralDataEncoding() { - LiteralData literalData = findLiteralData(); - if (literalData == null) { - return null; - } - return literalData.getFormat(); - } - - /** - * Find the {@link LiteralData} layer of an OpenPGP message. - * Usually, every message has a literal data packet, but for malformed messages this method might still - * return
null
. - * - * @return literal data - */ - private @Nullable LiteralData findLiteralData() { - Nested nested = message.getChild(); - if (nested == null) { - return null; - } - - while (nested != null && nested.hasNestedChild()) { - Layer layer = (Layer) nested; - nested = layer.getChild(); - } - return (LiteralData) nested; - } - - /** - * Return the {@link SubkeyIdentifier} of the decryption key that was used to decrypt the outermost encryption - * layer. - * If the message was unencrypted, this might return
null
. - * - * @return decryption key - */ - public SubkeyIdentifier getDecryptionKey() { - return firstOrNull(map(getEncryptionLayers(), encryptedData -> encryptedData.decryptionKey)); - } - - public boolean isVerifiedSigned() { - return !getVerifiedSignatures().isEmpty(); - } - - public interface Packet { - - } - public abstract static class Layer implements Packet { - public static final int MAX_LAYER_DEPTH = 16; - protected final int depth; - protected final List verifiedDetachedSignatures = new ArrayList<>(); - protected final List rejectedDetachedSignatures = new ArrayList<>(); - protected final List verifiedOnePassSignatures = new ArrayList<>(); - protected final List rejectedOnePassSignatures = new ArrayList<>(); - protected final List verifiedPrependedSignatures = new ArrayList<>(); - protected final List rejectedPrependedSignatures = new ArrayList<>(); - protected Nested child; - - public Layer(int depth) { - this.depth = depth; - if (depth > MAX_LAYER_DEPTH) { - throw new MalformedOpenPgpMessageException("Maximum packet nesting depth (" + MAX_LAYER_DEPTH + ") exceeded."); - } - } - - /** - * Return the nested child element of this layer. - * Might return
null
, if this layer does not have a child element - * (e.g. if this is a {@link LiteralData} packet). - * - * @return child element - */ - public @Nullable Nested getChild() { - return child; - } - - /** - * Set the nested child element for this layer. - * - * @param child child element - */ - void setChild(Nested child) { - this.child = child; - } - - /** - * Return a list of all verified detached signatures of this layer. - * - * @return all verified detached signatures of this layer - */ - public List getVerifiedDetachedSignatures() { - return new ArrayList<>(verifiedDetachedSignatures); - } - - /** - * Return a list of all rejected detached signatures of this layer. - * - * @return all rejected detached signatures of this layer - */ - public List getRejectedDetachedSignatures() { - return new ArrayList<>(rejectedDetachedSignatures); - } - - /** - * Add a verified detached signature for this layer. - * - * @param signatureVerification verified detached signature - */ - void addVerifiedDetachedSignature(SignatureVerification signatureVerification) { - verifiedDetachedSignatures.add(signatureVerification); - } - - /** - * Add a rejected detached signature for this layer. - * - * @param failure rejected detached signature - */ - void addRejectedDetachedSignature(SignatureVerification.Failure failure) { - rejectedDetachedSignatures.add(failure); - } - - /** - * Return a list of all verified one-pass-signatures of this layer. - * - * @return all verified one-pass-signatures of this layer - */ - public List getVerifiedOnePassSignatures() { - return new ArrayList<>(verifiedOnePassSignatures); - } - - /** - * Return a list of all rejected one-pass-signatures of this layer. - * - * @return all rejected one-pass-signatures of this layer - */ - public List getRejectedOnePassSignatures() { - return new ArrayList<>(rejectedOnePassSignatures); - } - - /** - * Add a verified one-pass-signature for this layer. - * - * @param verifiedOnePassSignature verified one-pass-signature for this layer - */ - void addVerifiedOnePassSignature(SignatureVerification verifiedOnePassSignature) { - this.verifiedOnePassSignatures.add(verifiedOnePassSignature); - } - - /** - * Add a rejected one-pass-signature for this layer. - * - * @param rejected rejected one-pass-signature for this layer - */ - void addRejectedOnePassSignature(SignatureVerification.Failure rejected) { - this.rejectedOnePassSignatures.add(rejected); - } - - /** - * Return a list of all verified prepended signatures of this layer. - * - * @return all verified prepended signatures of this layer - */ - public List getVerifiedPrependedSignatures() { - return new ArrayList<>(verifiedPrependedSignatures); - } - - /** - * Return a list of all rejected prepended signatures of this layer. - * - * @return all rejected prepended signatures of this layer - */ - public List getRejectedPrependedSignatures() { - return new ArrayList<>(rejectedPrependedSignatures); - } - - /** - * Add a verified prepended signature for this layer. - * - * @param verified verified prepended signature - */ - void addVerifiedPrependedSignature(SignatureVerification verified) { - this.verifiedPrependedSignatures.add(verified); - } - - /** - * Add a rejected prepended signature for this layer. - * - * @param rejected rejected prepended signature - */ - void addRejectedPrependedSignature(SignatureVerification.Failure rejected) { - this.rejectedPrependedSignatures.add(rejected); - } - - } - - public interface Nested extends Packet { - boolean hasNestedChild(); - } - - public static class Message extends Layer { - - protected boolean cleartextSigned; - - public Message() { - super(0); - } - - /** - * Returns true, is the message is a signed message using the cleartext signature framework. - * - * @return
true
if message is cleartext-signed,
false
otherwise - * @see RFC4880 §7. Cleartext Signature Framework - */ - public boolean isCleartextSigned() { - return cleartextSigned; - } - - } - - public static class LiteralData implements Nested { - protected String fileName; - protected Date modificationDate; - protected StreamEncoding format; - - public LiteralData() { - this("", new Date(0L), StreamEncoding.BINARY); - } - - public LiteralData(@Nonnull String fileName, - @Nonnull Date modificationDate, - @Nonnull StreamEncoding format) { - this.fileName = fileName; - this.modificationDate = modificationDate; - this.format = format; - } - - /** - * Return the value of the filename field. - * An empty String
""
indicates no filename. - * - * @return filename - */ - public @Nonnull String getFileName() { - return fileName; - } - - /** - * Return the value of the modification date field. - * A special date
{@code new Date(0L)}
indicates no modification date. - * - * @return modification date - */ - public @Nonnull Date getModificationDate() { - return modificationDate; - } - - /** - * Return the value of the format field. - * - * @return format - */ - public @Nonnull StreamEncoding getFormat() { - return format; - } - - @Override - public boolean hasNestedChild() { - // A literal data packet MUST NOT have a child element, as its content is the plaintext - return false; - } - } - - public static class CompressedData extends Layer implements Nested { - protected final CompressionAlgorithm algorithm; - - public CompressedData(@Nonnull CompressionAlgorithm zip, int depth) { - super(depth); - this.algorithm = zip; - } - - /** - * Return the {@link CompressionAlgorithm} used to compress the packet. - * @return compression algorithm - */ - public @Nonnull CompressionAlgorithm getAlgorithm() { - return algorithm; - } - - @Override - public boolean hasNestedChild() { - // A compressed data packet MUST have a child element - return true; - } - } - - public static class EncryptedData extends Layer implements Nested { - protected final SymmetricKeyAlgorithm algorithm; - protected SubkeyIdentifier decryptionKey; - protected SessionKey sessionKey; - protected List recipients; - - public EncryptedData(@Nonnull SymmetricKeyAlgorithm algorithm, int depth) { - super(depth); - this.algorithm = algorithm; - } - - /** - * Return the {@link SymmetricKeyAlgorithm} used to encrypt the packet. - * @return symmetric encryption algorithm - */ - public @Nonnull SymmetricKeyAlgorithm getAlgorithm() { - return algorithm; - } - - /** - * Return the {@link SessionKey} used to decrypt the packet. - * - * @return session key - */ - public @Nonnull SessionKey getSessionKey() { - return sessionKey; - } - - /** - * Return a list of all recipient key ids to which the packet was encrypted for. - * - * @return recipients - */ - public @Nonnull List getRecipients() { - if (recipients == null) { - return new ArrayList<>(); - } - return new ArrayList<>(recipients); - } - - @Override - public boolean hasNestedChild() { - // An encrypted data packet MUST have a child element - return true; - } - } - - - private abstract static class LayerIterator implements Iterator { - private Nested current; - Layer last = null; - Message parent; - - LayerIterator(@Nonnull Message message) { - super(); - this.parent = message; - this.current = message.getChild(); - if (matches(current)) { - last = (Layer) current; - } - } - - @Override - public boolean hasNext() { - if (parent != null && matches(parent)) { - return true; - } - if (last == null) { - findNext(); - } - return last != null; - } - - @Override - public O next() { - if (parent != null && matches(parent)) { - O property = getProperty(parent); - parent = null; - return property; - } - if (last == null) { - findNext(); - } - if (last != null) { - O property = getProperty(last); - last = null; - return property; - } - throw new NoSuchElementException(); - } - - private void findNext() { - while (current != null && current instanceof Layer) { - current = ((Layer) current).getChild(); - if (matches(current)) { - last = (Layer) current; - break; - } - } - } - - abstract boolean matches(Packet layer); - - abstract O getProperty(Layer last); - } - - private static Iterator map(Iterator from, Function mapping) { - return new Iterator() { - @Override - public boolean hasNext() { - return from.hasNext(); - } - - @Override - public B next() { - return mapping.apply(from.next()); - } - }; - } - - public interface Function { - B apply(A item); - } - - private static @Nullable A firstOrNull(Iterator iterator) { - if (iterator.hasNext()) { - return iterator.next(); - } - return null; - } - - private static @Nonnull A firstOr(Iterator iterator, A item) { - if (iterator.hasNext()) { - return iterator.next(); - } - return item; - } -} 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 9b6f4a1e..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MissingPublicKeyCallback.java +++ /dev/null @@ -1,33 +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. - * - * @param keyId ID of the missing signing (sub)key - * - * @return keyring containing the key or null - * - * @see RFC - */ - @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 index 028fa666..3522f509 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpInputStream.java @@ -32,6 +32,7 @@ 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; @@ -45,6 +46,9 @@ 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") @@ -205,8 +209,8 @@ public class OpenPgpInputStream extends BufferedInputStream { } try { - SignatureType.valueOf(sigType); - } catch (IllegalArgumentException e) { + SignatureType.requireFromCode(sigType); + } catch (NoSuchElementException e) { return; } @@ -233,8 +237,8 @@ public class OpenPgpInputStream extends BufferedInputStream { if (opsVersion == 3) { int opsSigType = bcpgIn.read(); try { - SignatureType.valueOf(opsSigType); - } catch (IllegalArgumentException e) { + SignatureType.requireFromCode(opsSigType); + } catch (NoSuchElementException e) { return; } int opsHashAlg = bcpgIn.read(); diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java deleted file mode 100644 index 7fe11bbf..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ /dev/null @@ -1,1133 +0,0 @@ -// SPDX-FileCopyrightText: 2022 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 java.util.ArrayList; -import java.util.Collection; -import java.util.Date; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.Stack; -import javax.annotation.Nonnull; - -import org.bouncycastle.bcpg.ArmoredInputStream; -import org.bouncycastle.bcpg.BCPGInputStream; -import org.bouncycastle.bcpg.S2K; -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.PGPLiteralData; -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.PGPSessionKeyEncryptedData; -import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.PGPSignatureList; -import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; -import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; -import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; -import org.bouncycastle.openpgp.operator.SessionKeyDataDecryptorFactory; -import org.bouncycastle.util.io.TeeInputStream; -import org.pgpainless.PGPainless; -import org.pgpainless.algorithm.CompressionAlgorithm; -import org.pgpainless.algorithm.EncryptionPurpose; -import org.pgpainless.algorithm.OpenPgpPacket; -import org.pgpainless.algorithm.StreamEncoding; -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; -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.decryption_verification.cleartext_signatures.ClearsignedMessageUtil; -import org.pgpainless.decryption_verification.cleartext_signatures.MultiPassStrategy; -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.info.KeyRingInfo; -import org.pgpainless.key.protection.SecretKeyRingProtector; -import org.pgpainless.key.protection.UnlockSecretKey; -import org.pgpainless.key.util.KeyIdUtil; -import org.pgpainless.key.util.KeyRingUtils; -import org.pgpainless.policy.Policy; -import org.pgpainless.signature.SignatureUtils; -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.Passphrase; -import org.pgpainless.util.SessionKey; -import org.pgpainless.util.Tuple; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class OpenPgpMessageInputStream extends DecryptionStream { - - private static final Logger LOGGER = LoggerFactory.getLogger(OpenPgpMessageInputStream.class); - - // Options to consume the data - protected final ConsumerOptions options; - - private final Policy policy; - // Pushdown Automaton to verify validity of OpenPGP packet sequence in an OpenPGP message - protected final PDA syntaxVerifier = new PDA(); - // InputStream of OpenPGP packets - protected TeeBCPGInputStream packetInputStream; - // InputStream of a data packet containing nested data - protected InputStream nestedInputStream; - - private boolean closed = false; - - private final Signatures signatures; - private final MessageMetadata.Layer metadata; - - /** - * Create an {@link OpenPgpMessageInputStream} suitable for decryption and verification of - * OpenPGP messages and signatures. - * This constructor will use the global PGPainless {@link Policy}. - * - * @param inputStream underlying input stream - * @param options options for consuming the stream - * @return input stream that consumes OpenPGP messages - * - * @throws IOException in case of an IO error - * @throws PGPException in case of an OpenPGP error - */ - public static OpenPgpMessageInputStream create(@Nonnull InputStream inputStream, - @Nonnull ConsumerOptions options) - throws IOException, PGPException { - return create(inputStream, options, PGPainless.getPolicy()); - } - - /** - * Create an {@link OpenPgpMessageInputStream} suitable for decryption and verification of - * OpenPGP messages and signatures. - * This factory method takes a custom {@link Policy} instead of using the global policy object. - * - * @param inputStream underlying input stream containing the OpenPGP message - * @param options options for consuming the message - * @param policy policy for acceptable algorithms etc. - * @return input stream that consumes OpenPGP messages - * - * @throws PGPException in case of an OpenPGP error - * @throws IOException in case of an IO error - */ - public static OpenPgpMessageInputStream create(@Nonnull InputStream inputStream, - @Nonnull ConsumerOptions options, - @Nonnull Policy policy) - throws PGPException, IOException { - return create(inputStream, options, new MessageMetadata.Message(), policy); - } - - protected static OpenPgpMessageInputStream create(@Nonnull InputStream inputStream, - @Nonnull ConsumerOptions options, - @Nonnull MessageMetadata.Layer metadata, - @Nonnull Policy policy) - throws IOException, PGPException { - OpenPgpInputStream openPgpIn = new OpenPgpInputStream(inputStream); - openPgpIn.reset(); - - if (openPgpIn.isNonOpenPgp() || options.isForceNonOpenPgpData()) { - return new OpenPgpMessageInputStream(Type.non_openpgp, - openPgpIn, options, metadata, policy); - } - - if (openPgpIn.isBinaryOpenPgp()) { - // Simply consume OpenPGP message - return new OpenPgpMessageInputStream(Type.standard, - openPgpIn, options, metadata, policy); - } - - if (openPgpIn.isAsciiArmored()) { - ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(openPgpIn); - if (armorIn.isClearText()) { - ((MessageMetadata.Message) metadata).cleartextSigned = true; - return new OpenPgpMessageInputStream(Type.cleartext_signed, - armorIn, options, metadata, policy); - } else { - // Simply consume dearmored OpenPGP message - return new OpenPgpMessageInputStream(Type.standard, - armorIn, options, metadata, policy); - } - } else { - throw new AssertionError("Cannot deduce type of data."); - } - } - - protected OpenPgpMessageInputStream(@Nonnull InputStream inputStream, - @Nonnull ConsumerOptions options, - @Nonnull MessageMetadata.Layer metadata, - @Nonnull Policy policy) - throws PGPException, IOException { - super(); - - this.policy = policy; - this.options = options; - this.metadata = metadata; - this.signatures = new Signatures(options); - - // Add detached signatures only on the outermost OpenPgpMessageInputStream - if (metadata instanceof MessageMetadata.Message) { - this.signatures.addDetachedSignatures(options.getDetachedSignatures()); - } - - // tee out packet bytes for signature verification - packetInputStream = new TeeBCPGInputStream(BCPGInputStream.wrap(inputStream), signatures); - - // *omnomnom* - consumePackets(); - } - - enum Type { - standard, - cleartext_signed, - non_openpgp - } - - protected OpenPgpMessageInputStream(@Nonnull Type type, - @Nonnull InputStream inputStream, - @Nonnull ConsumerOptions options, - @Nonnull MessageMetadata.Layer metadata, - @Nonnull Policy policy) throws PGPException, IOException { - super(); - this.policy = policy; - this.options = options; - this.metadata = metadata; - this.signatures = new Signatures(options); - - if (metadata instanceof MessageMetadata.Message) { - this.signatures.addDetachedSignatures(options.getDetachedSignatures()); - } - - switch (type) { - - // Binary OpenPGP Message - case standard: - // tee out packet bytes for signature verification - packetInputStream = new TeeBCPGInputStream(BCPGInputStream.wrap(inputStream), this.signatures); - - // *omnomnom* - consumePackets(); - break; - - // Cleartext Signature Framework (probably signed message) - case cleartext_signed: - MultiPassStrategy multiPassStrategy = options.getMultiPassStrategy(); - PGPSignatureList detachedSignatures = ClearsignedMessageUtil - .detachSignaturesFromInbandClearsignedMessage( - inputStream, multiPassStrategy.getMessageOutputStream()); - - for (PGPSignature signature : detachedSignatures) { - signatures.addDetachedSignature(signature); - } - - options.forceNonOpenPgpData(); - nestedInputStream = new TeeInputStream(multiPassStrategy.getMessageInputStream(), this.signatures); - break; - - // Non-OpenPGP Data (e.g. detached signature verification) - case non_openpgp: - packetInputStream = null; - nestedInputStream = new TeeInputStream(inputStream, this.signatures); - break; - } - } - - /** - * Consume OpenPGP packets from the current {@link BCPGInputStream}. - * Once an OpenPGP packet with nested data (Literal Data, Compressed Data, Encrypted Data) is reached, - * set
nestedInputStream
to the nested stream and breaks the loop. - * The nested stream is either a simple {@link InputStream} (in case of Literal Data), or another - * {@link OpenPgpMessageInputStream} in case of Compressed and Encrypted Data. - * Once the nested data is processed, this method is called again to consume the remainder - * of packets following the nested data packet. - * - * @throws IOException in case of an IO error - * @throws PGPException in case of an OpenPGP error - * @throws MissingDecryptionMethodException if there is an encrypted data packet which cannot be decrypted - * due to missing decryption methods (no key, no password, no sessionkey) - * @throws MalformedOpenPgpMessageException if the message is made of an invalid packet sequence which - * does not follow the packet syntax of RFC4880. - */ - private void consumePackets() - throws IOException, PGPException { - OpenPgpPacket nextPacket; - if (packetInputStream == null) { - return; - } - - loop: // we break this when we enter nested packets and later resume - while ((nextPacket = packetInputStream.nextPacketTag()) != null) { - signatures.nextPacket(nextPacket); - switch (nextPacket) { - - // Literal Data - the literal data content is the new input stream - case LIT: - processLiteralData(); - break loop; - - // Compressed Data - the content contains another OpenPGP message - case COMP: - processCompressedData(); - break loop; - - // One Pass Signature - case OPS: - processOnePassSignature(); - break; - - // Signature - either prepended to the message, or corresponding to a One Pass Signature - case SIG: - processSignature(); - break; - - // Encrypted Data (ESKs and SED/SEIPD are parsed the same by BC) - case PKESK: - case SKESK: - case SED: - case SEIPD: - if (processEncryptedData()) { - // Successfully decrypted, enter nested content - break loop; - } - - throw new MissingDecryptionMethodException("No working decryption method found."); - - // Marker Packets need to be skipped and ignored - case MARKER: - LOGGER.debug("Skipping Marker Packet"); - packetInputStream.readMarker(); - break; - - // Key Packets are illegal in this context - case SK: - case PK: - case SSK: - case PSK: - case TRUST: - case UID: - case UATTR: - throw new MalformedOpenPgpMessageException("Illegal Packet in Stream: " + nextPacket); - - // MDC packet is usually processed by PGPEncryptedDataList, so it is very likely we encounter this - // packet out of order - case MDC: - throw new MalformedOpenPgpMessageException("Unexpected Packet in Stream: " + nextPacket); - - // Experimental Packets are not supported - case EXP_1: - case EXP_2: - case EXP_3: - case EXP_4: - throw new MalformedOpenPgpMessageException("Unsupported Packet in Stream: " + nextPacket); - } - } - } - - private void processLiteralData() throws IOException { - LOGGER.debug("Literal Data Packet at depth " + metadata.depth + " encountered"); - syntaxVerifier.next(InputSymbol.LiteralData); - PGPLiteralData literalData = packetInputStream.readLiteralData(); - // Extract Metadata - this.metadata.setChild(new MessageMetadata.LiteralData( - literalData.getFileName(), - literalData.getModificationTime(), - StreamEncoding.requireFromCode(literalData.getFormat()))); - - nestedInputStream = literalData.getDataStream(); - } - - private void processCompressedData() throws IOException, PGPException { - syntaxVerifier.next(InputSymbol.CompressedData); - signatures.enterNesting(); - PGPCompressedData compressedData = packetInputStream.readCompressedData(); - // Extract Metadata - MessageMetadata.CompressedData compressionLayer = new MessageMetadata.CompressedData( - CompressionAlgorithm.fromId(compressedData.getAlgorithm()), - metadata.depth + 1); - - LOGGER.debug("Compressed Data Packet (" + compressionLayer.algorithm + ") at depth " + metadata.depth + " encountered"); - InputStream decompressed = compressedData.getDataStream(); - nestedInputStream = new OpenPgpMessageInputStream(decompressed, options, compressionLayer, policy); - } - - private void processOnePassSignature() throws PGPException, IOException { - syntaxVerifier.next(InputSymbol.OnePassSignature); - PGPOnePassSignature onePassSignature = packetInputStream.readOnePassSignature(); - LOGGER.debug("One-Pass-Signature Packet by key " + KeyIdUtil.formatKeyId(onePassSignature.getKeyID()) + - " at depth " + metadata.depth + " encountered"); - signatures.addOnePassSignature(onePassSignature); - } - - private void processSignature() throws PGPException, IOException { - // true if Signature corresponds to OnePassSignature - boolean isSigForOPS = syntaxVerifier.peekStack() == StackSymbol.ops; - syntaxVerifier.next(InputSymbol.Signature); - PGPSignature signature; - try { - signature = packetInputStream.readSignature(); - } catch (UnsupportedPacketVersionException e) { - LOGGER.debug("Unsupported Signature at depth " + metadata.depth + " encountered.", e); - return; - } - - long keyId = SignatureUtils.determineIssuerKeyId(signature); - if (isSigForOPS) { - LOGGER.debug("Signature Packet corresponding to One-Pass-Signature by key " + - KeyIdUtil.formatKeyId(keyId) + - " at depth " + metadata.depth + " encountered"); - signatures.leaveNesting(); // TODO: Only leave nesting if all OPSs of the nesting layer are dealt with - signatures.addCorrespondingOnePassSignature(signature, metadata, policy); - } else { - LOGGER.debug("Prepended Signature Packet by key " + - KeyIdUtil.formatKeyId(keyId) + - " at depth " + metadata.depth + " encountered"); - signatures.addPrependedSignature(signature); - } - } - - private boolean processEncryptedData() throws IOException, PGPException { - LOGGER.debug("Symmetrically Encrypted Data Packet at depth " + metadata.depth + " encountered"); - syntaxVerifier.next(InputSymbol.EncryptedData); - PGPEncryptedDataList encDataList = packetInputStream.readEncryptedDataList(); - - if (!encDataList.isIntegrityProtected()) { - LOGGER.warn("Symmetrically Encrypted Data Packet is not integrity-protected."); - if (!options.isIgnoreMDCErrors()) { - throw new MessageNotIntegrityProtectedException(); - } - } - - SortedESKs esks = new 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 (SubkeyIdentifier subkeyIdentifier : options.getCustomDecryptorFactories().keySet()) { - LOGGER.debug("Attempt decryption with custom decryptor factory with key " + subkeyIdentifier); - PublicKeyDataDecryptorFactory decryptorFactory = options.getCustomDecryptorFactories().get(subkeyIdentifier); - for (PGPPublicKeyEncryptedData pkesk : esks.pkesks) { - // find matching PKESK - if (pkesk.getKeyID() != subkeyIdentifier.getSubkeyId()) { - continue; - } - - // attempt decryption - if (decryptPKESKAndStream(esks, subkeyIdentifier, decryptorFactory, pkesk)) { - return true; - } - } - } - - // Try provided session key - if (options.getSessionKey() != null) { - LOGGER.debug("Attempt decryption with provided session key"); - SessionKey sessionKey = options.getSessionKey(); - throwIfUnacceptable(sessionKey.getAlgorithm()); - - SessionKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() - .getSessionKeyDataDecryptorFactory(sessionKey); - MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( - sessionKey.getAlgorithm(), metadata.depth + 1); - - PGPSessionKeyEncryptedData sessionKeyEncryptedData = encDataList.extractSessionKeyEncryptedData(); - try { - InputStream decrypted = sessionKeyEncryptedData.getDataStream(decryptorFactory); - encryptedData.sessionKey = sessionKey; - IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, sessionKeyEncryptedData, options); - nestedInputStream = new OpenPgpMessageInputStream(integrityProtected, options, encryptedData, policy); - LOGGER.debug("Successfully decrypted data with provided session key"); - return true; - } catch (PGPException e) { - // Session key mismatch? - LOGGER.debug("Decryption using provided session key failed. Mismatched session key and message?", e); - } - } - - // Try passwords - for (Passphrase passphrase : options.getDecryptionPassphrases()) { - for (PGPPBEEncryptedData skesk : esks.skesks) { - LOGGER.debug("Attempt decryption with provided passphrase"); - SymmetricKeyAlgorithm encapsulationAlgorithm = SymmetricKeyAlgorithm.requireFromId(skesk.getAlgorithm()); - try { - throwIfUnacceptable(encapsulationAlgorithm); - } catch (UnacceptableAlgorithmException e) { - LOGGER.debug("Skipping SKESK with unacceptable encapsulation algorithm", e); - continue; - } - - PBEDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() - .getPBEDataDecryptorFactory(passphrase); - if (decryptSKESKAndStream(esks, skesk, decryptorFactory)) { - return true; - } - } - } - - List> postponedDueToMissingPassphrase = new ArrayList<>(); - - // Try (known) secret keys - for (PGPPublicKeyEncryptedData pkesk : esks.pkesks) { - long keyId = pkesk.getKeyID(); - LOGGER.debug("Encountered PKESK for recipient " + KeyIdUtil.formatKeyId(keyId)); - PGPSecretKeyRing decryptionKeys = getDecryptionKey(keyId); - if (decryptionKeys == null) { - LOGGER.debug("Skipping PKESK because no matching key " + KeyIdUtil.formatKeyId(keyId) + " was provided"); - continue; - } - PGPSecretKey secretKey = decryptionKeys.getSecretKey(keyId); - SubkeyIdentifier decryptionKeyId = new SubkeyIdentifier(decryptionKeys, secretKey.getKeyID()); - if (hasUnsupportedS2KSpecifier(secretKey, decryptionKeyId)) { - continue; - } - LOGGER.debug("Attempt decryption using secret key " + decryptionKeyId); - - SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKeys); - // Postpone keys with missing passphrase - if (!protector.hasPassphraseFor(keyId)) { - LOGGER.debug("Missing passphrase for key " + decryptionKeyId + ". Postponing decryption until all other keys were tried"); - postponedDueToMissingPassphrase.add(new Tuple<>(secretKey, pkesk)); - continue; - } - - PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKey, protector); - if (decryptWithPrivateKey(esks, privateKey, decryptionKeyId, pkesk)) { - return true; - } - } - - // try anonymous secret keys - for (PGPPublicKeyEncryptedData pkesk : esks.anonPkesks) { - for (Tuple decryptionKeyCandidate : findPotentialDecryptionKeys(pkesk)) { - PGPSecretKeyRing decryptionKeys = decryptionKeyCandidate.getA(); - PGPSecretKey secretKey = decryptionKeyCandidate.getB(); - SubkeyIdentifier decryptionKeyId = new SubkeyIdentifier(decryptionKeys, secretKey.getKeyID()); - if (hasUnsupportedS2KSpecifier(secretKey, decryptionKeyId)) { - continue; - } - LOGGER.debug("Attempt decryption of anonymous PKESK with key " + decryptionKeyId); - SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKeyCandidate.getA()); - if (!protector.hasPassphraseFor(secretKey.getKeyID())) { - LOGGER.debug("Missing passphrase for key " + decryptionKeyId + ". Postponing decryption until all other keys were tried."); - postponedDueToMissingPassphrase.add(new Tuple<>(secretKey, pkesk)); - continue; - } - - PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKey, 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 - Set keyIds = new HashSet<>(); - for (Tuple k : postponedDueToMissingPassphrase) { - PGPSecretKey key = k.getA(); - PGPSecretKeyRing keys = getDecryptionKey(key.getKeyID()); - keyIds.add(new SubkeyIdentifier(keys, key.getKeyID())); - } - if (!keyIds.isEmpty()) { - throw new MissingPassphraseException(keyIds); - } - } else if (options.getMissingKeyPassphraseStrategy() == MissingKeyPassphraseStrategy.INTERACTIVE) { - for (PGPPublicKeyEncryptedData pkesk : esks.pkesks) { - // Interactive mode: Fire protector callbacks to get passphrases interactively - for (Tuple missingPassphrases : postponedDueToMissingPassphrase) { - PGPSecretKey secretKey = missingPassphrases.getA(); - long keyId = secretKey.getKeyID(); - PGPSecretKeyRing decryptionKey = getDecryptionKey(keyId); - SubkeyIdentifier decryptionKeyId = new SubkeyIdentifier(decryptionKey, keyId); - if (hasUnsupportedS2KSpecifier(secretKey, decryptionKeyId)) { - continue; - } - - LOGGER.debug("Attempt decryption with key " + decryptionKeyId + " while interactively requesting its passphrase"); - SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKey); - PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKey, protector); - if (decryptWithPrivateKey(esks, privateKey, decryptionKeyId, pkesk)) { - return true; - } - } - } - } else { - throw new 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 boolean decryptWithPrivateKey(SortedESKs esks, - PGPPrivateKey privateKey, - SubkeyIdentifier decryptionKeyId, - PGPPublicKeyEncryptedData pkesk) - throws PGPException, IOException { - PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() - .getPublicKeyDataDecryptorFactory(privateKey); - return decryptPKESKAndStream(esks, decryptionKeyId, decryptorFactory, pkesk); - } - - private static boolean hasUnsupportedS2KSpecifier(PGPSecretKey secretKey, SubkeyIdentifier decryptionKeyId) { - S2K s2K = secretKey.getS2K(); - if (s2K != null) { - int s2kType = s2K.getType(); - if (s2kType >= 100 && s2kType <= 110) { - LOGGER.debug("Skipping PKESK because key " + decryptionKeyId + " has unsupported private S2K specifier " + s2kType); - return true; - } - } - return false; - } - - private boolean decryptSKESKAndStream(SortedESKs esks, - PGPPBEEncryptedData symEsk, - PBEDataDecryptorFactory decryptorFactory) - throws IOException, UnacceptableAlgorithmException { - try { - InputStream decrypted = symEsk.getDataStream(decryptorFactory); - SessionKey sessionKey = new SessionKey(symEsk.getSessionKey(decryptorFactory)); - throwIfUnacceptable(sessionKey.getAlgorithm()); - MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( - sessionKey.getAlgorithm(), metadata.depth + 1); - encryptedData.sessionKey = sessionKey; - encryptedData.recipients = new ArrayList<>(); - for (PGPPublicKeyEncryptedData pkesk : esks.pkesks) { - encryptedData.recipients.add(pkesk.getKeyID()); - } - LOGGER.debug("Successfully decrypted data with passphrase"); - IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, symEsk, options); - nestedInputStream = new OpenPgpMessageInputStream(integrityProtected, options, encryptedData, policy); - return true; - } catch (UnacceptableAlgorithmException e) { - throw e; - } catch (PGPException e) { - LOGGER.debug("Decryption of encrypted data packet using password failed. Password mismatch?", e); - } - return false; - } - - private boolean decryptPKESKAndStream(SortedESKs esks, - SubkeyIdentifier decryptionKeyId, - PublicKeyDataDecryptorFactory decryptorFactory, - PGPPublicKeyEncryptedData asymEsk) - throws IOException, UnacceptableAlgorithmException { - try { - InputStream decrypted = asymEsk.getDataStream(decryptorFactory); - SessionKey sessionKey = new SessionKey(asymEsk.getSessionKey(decryptorFactory)); - throwIfUnacceptable(sessionKey.getAlgorithm()); - - MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( - SymmetricKeyAlgorithm.requireFromId(asymEsk.getSymmetricAlgorithm(decryptorFactory)), - metadata.depth + 1); - encryptedData.decryptionKey = decryptionKeyId; - encryptedData.sessionKey = sessionKey; - encryptedData.recipients = new ArrayList<>(); - for (PGPPublicKeyEncryptedData pkesk : esks.pkesks) { - encryptedData.recipients.add(pkesk.getKeyID()); - } - - LOGGER.debug("Successfully decrypted data with key " + decryptionKeyId); - IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, asymEsk, options); - nestedInputStream = new OpenPgpMessageInputStream(integrityProtected, options, encryptedData, policy); - return true; - } catch (UnacceptableAlgorithmException e) { - throw e; - } catch (PGPException e) { - LOGGER.debug("Decryption of encrypted data packet using secret key failed.", e); - } - return false; - } - - private void throwIfUnacceptable(SymmetricKeyAlgorithm algorithm) - throws UnacceptableAlgorithmException { - if (!policy.getSymmetricKeyDecryptionAlgorithmPolicy().isAcceptable(algorithm)) { - throw new UnacceptableAlgorithmException("Symmetric-Key algorithm " + algorithm + " is not acceptable for message decryption."); - } - } - - private List> findPotentialDecryptionKeys(PGPPublicKeyEncryptedData pkesk) { - int algorithm = pkesk.getAlgorithm(); - List> decryptionKeyCandidates = new ArrayList<>(); - - for (PGPSecretKeyRing secretKeys : options.getDecryptionKeys()) { - KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); - for (PGPPublicKey publicKey : info.getEncryptionSubkeys(EncryptionPurpose.ANY)) { - if (publicKey.getAlgorithm() == algorithm && info.isSecretKeyAvailable(publicKey.getKeyID())) { - PGPSecretKey candidate = secretKeys.getSecretKey(publicKey.getKeyID()); - decryptionKeyCandidates.add(new Tuple<>(secretKeys, candidate)); - } - } - } - return decryptionKeyCandidates; - } - - private PGPSecretKeyRing getDecryptionKey(long keyID) { - for (PGPSecretKeyRing secretKeys : options.getDecryptionKeys()) { - PGPSecretKey decryptionKey = secretKeys.getSecretKey(keyID); - if (decryptionKey == null) { - continue; - } - - KeyRingInfo info = new KeyRingInfo(secretKeys, policy, new Date()); - List encryptionKeys = info.getEncryptionSubkeys(EncryptionPurpose.ANY); - for (PGPPublicKey key : encryptionKeys) { - if (key.getKeyID() == keyID) { - return secretKeys; - } - } - - LOGGER.debug("Subkey " + Long.toHexString(keyID) + " cannot be used for decryption."); - } - return null; - } - - @Override - public int read() throws IOException { - if (nestedInputStream == null) { - if (packetInputStream != null) { - syntaxVerifier.assertValid(); - } - return -1; - } - - int r; - try { - r = nestedInputStream.read(); - } catch (IOException e) { - r = -1; - } - boolean eos = r == -1; - if (!eos) { - byte b = (byte) r; - signatures.updateLiteral(b); - } else { - nestedInputStream.close(); - collectMetadata(); - nestedInputStream = null; - - if (packetInputStream != null) { - try { - consumePackets(); - } catch (PGPException e) { - throw new RuntimeException(e); - } - } - signatures.finish(metadata, policy); - } - return r; - } - - @Override - public int read(@Nonnull byte[] b, int off, int len) - throws IOException { - if (nestedInputStream == null) { - if (packetInputStream != null) { - syntaxVerifier.assertValid(); - } - return -1; - } - - int 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 (PGPException e) { - throw new RuntimeException(e); - } - } - signatures.finish(metadata, policy); - } - return r; - } - - @Override - public void close() throws IOException { - super.close(); - if (closed) { - if (packetInputStream != null) { - syntaxVerifier.assertValid(); - } - return; - } - - if (nestedInputStream != null) { - nestedInputStream.close(); - collectMetadata(); - nestedInputStream = null; - } - - try { - consumePackets(); - } catch (PGPException e) { - throw new RuntimeException(e); - } - - if (packetInputStream != null) { - syntaxVerifier.next(InputSymbol.EndOfSequence); - syntaxVerifier.assertValid(); - packetInputStream.close(); - } - closed = true; - } - - private void collectMetadata() { - if (nestedInputStream instanceof OpenPgpMessageInputStream) { - OpenPgpMessageInputStream child = (OpenPgpMessageInputStream) nestedInputStream; - this.metadata.setChild((MessageMetadata.Nested) child.metadata); - } - } - - public MessageMetadata getMetadata() { - if (!closed) { - throw new IllegalStateException("Stream must be closed before access to metadata can be granted."); - } - - return new MessageMetadata((MessageMetadata.Message) metadata); - } - - private static class SortedESKs { - - private final List skesks = new ArrayList<>(); - private final List pkesks = new ArrayList<>(); - private final List anonPkesks = new ArrayList<>(); - - SortedESKs(PGPEncryptedDataList esks) { - for (PGPEncryptedData esk : esks) { - if (esk instanceof PGPPBEEncryptedData) { - skesks.add((PGPPBEEncryptedData) esk); - } - else if (esk instanceof PGPPublicKeyEncryptedData) { - PGPPublicKeyEncryptedData pkesk = (PGPPublicKeyEncryptedData) esk; - if (pkesk.getKeyID() != 0) { - pkesks.add(pkesk); - } else { - anonPkesks.add(pkesk); - } - } else { - throw new IllegalArgumentException("Unknown ESK class type."); - } - } - } - - public List all() { - List esks = new ArrayList<>(); - esks.addAll(skesks); - esks.addAll(pkesks); - esks.addAll(anonPkesks); - return esks; - } - } - - // In 'OPS LIT("Foo") SIG', OPS is only updated with "Foo" - // In 'OPS[1] OPS LIT("Foo") SIG SIG', OPS[1] (nested) is updated with OPS LIT("Foo") SIG. - // Therefore, we need to handle the innermost signature layer differently when updating with Literal data. - // Furthermore, For 'OPS COMP(LIT("Foo")) SIG', the signature is updated with "Foo". CHAOS!!! - private static final class Signatures extends OutputStream { - final ConsumerOptions options; - final List detachedSignatures; - final List prependedSignatures; - final List onePassSignatures; - final Stack> opsUpdateStack; - List literalOPS = new ArrayList<>(); - final List correspondingSignatures; - final List prependedSignaturesWithMissingCert = new ArrayList<>(); - final List inbandSignaturesWithMissingCert = new ArrayList<>(); - final List detachedSignaturesWithMissingCert = new ArrayList<>(); - boolean isLiteral = true; - - private Signatures(ConsumerOptions options) { - this.options = options; - this.detachedSignatures = new ArrayList<>(); - this.prependedSignatures = new ArrayList<>(); - this.onePassSignatures = new ArrayList<>(); - this.opsUpdateStack = new Stack<>(); - this.correspondingSignatures = new ArrayList<>(); - } - - void addDetachedSignatures(Collection signatures) { - for (PGPSignature signature : signatures) { - addDetachedSignature(signature); - } - } - - void addDetachedSignature(PGPSignature signature) { - SignatureCheck check = initializeSignature(signature); - long keyId = SignatureUtils.determineIssuerKeyId(signature); - if (check != null) { - detachedSignatures.add(check); - } else { - LOGGER.debug("No suitable certificate for verification of signature by key " + KeyIdUtil.formatKeyId(keyId) + " found."); - this.detachedSignaturesWithMissingCert.add(new SignatureVerification.Failure( - new SignatureVerification(signature, null), - new SignatureValidationException("Missing verification key") - )); - } - } - - void addPrependedSignature(PGPSignature signature) { - SignatureCheck check = initializeSignature(signature); - long keyId = SignatureUtils.determineIssuerKeyId(signature); - if (check != null) { - this.prependedSignatures.add(check); - } else { - LOGGER.debug("No suitable certificate for verification of signature by key " + KeyIdUtil.formatKeyId(keyId) + " found."); - this.prependedSignaturesWithMissingCert.add(new SignatureVerification.Failure( - new SignatureVerification(signature, null), - new SignatureValidationException("Missing verification key") - )); - } - } - - SignatureCheck initializeSignature(PGPSignature signature) { - long keyId = SignatureUtils.determineIssuerKeyId(signature); - PGPPublicKeyRing certificate = findCertificate(keyId); - if (certificate == null) { - return null; - } - - SubkeyIdentifier verifierKey = new SubkeyIdentifier(certificate, keyId); - initialize(signature, certificate, keyId); - return new SignatureCheck(signature, certificate, verifierKey); - } - - void addOnePassSignature(PGPOnePassSignature signature) { - PGPPublicKeyRing certificate = findCertificate(signature.getKeyID()); - - if (certificate != null) { - OnePassSignatureCheck ops = new OnePassSignatureCheck(signature, certificate); - initialize(signature, certificate); - onePassSignatures.add(ops); - - literalOPS.add(ops); - } - if (signature.isContaining()) { - enterNesting(); - } - } - - void addCorrespondingOnePassSignature(PGPSignature signature, MessageMetadata.Layer layer, Policy policy) { - boolean found = false; - long keyId = SignatureUtils.determineIssuerKeyId(signature); - for (int i = onePassSignatures.size() - 1; i >= 0; i--) { - OnePassSignatureCheck onePassSignature = onePassSignatures.get(i); - if (onePassSignature.getOnePassSignature().getKeyID() != keyId) { - continue; - } - found = true; - - if (onePassSignature.getSignature() != null) { - continue; - } - - onePassSignature.setSignature(signature); - SignatureVerification verification = new SignatureVerification(signature, - new SubkeyIdentifier(onePassSignature.getVerificationKeys(), onePassSignature.getOnePassSignature().getKeyID())); - - try { - SignatureValidator.signatureWasCreatedInBounds(options.getVerifyNotBefore(), options.getVerifyNotAfter()) - .verify(signature); - CertificateValidator.validateCertificateAndVerifyOnePassSignature(onePassSignature, policy); - LOGGER.debug("Acceptable signature by key " + verification.getSigningKey()); - layer.addVerifiedOnePassSignature(verification); - } catch (SignatureValidationException e) { - LOGGER.debug("Rejected signature by key " + verification.getSigningKey(), e); - layer.addRejectedOnePassSignature(new SignatureVerification.Failure(verification, e)); - } - break; - } - - if (!found) { - LOGGER.debug("No suitable certificate for verification of signature by key " + KeyIdUtil.formatKeyId(keyId) + " found."); - inbandSignaturesWithMissingCert.add(new SignatureVerification.Failure( - new SignatureVerification(signature, null), - new SignatureValidationException("Missing verification key"))); - } - } - - void enterNesting() { - opsUpdateStack.push(literalOPS); - literalOPS = new ArrayList<>(); - } - - void leaveNesting() { - if (opsUpdateStack.isEmpty()) { - return; - } - opsUpdateStack.pop(); - } - - private static void initialize(@Nonnull PGPSignature signature, @Nonnull PGPPublicKeyRing certificate, long keyId) { - PGPContentVerifierBuilderProvider verifierProvider = ImplementationFactory.getInstance() - .getPGPContentVerifierBuilderProvider(); - try { - signature.init(verifierProvider, certificate.getPublicKey(keyId)); - } catch (PGPException e) { - throw new RuntimeException(e); - } - } - - private static void initialize(@Nonnull PGPOnePassSignature ops, @Nonnull PGPPublicKeyRing certificate) { - PGPContentVerifierBuilderProvider verifierProvider = ImplementationFactory.getInstance() - .getPGPContentVerifierBuilderProvider(); - try { - ops.init(verifierProvider, certificate.getPublicKey(ops.getKeyID())); - } catch (PGPException e) { - throw new RuntimeException(e); - } - } - - private PGPPublicKeyRing findCertificate(long keyId) { - PGPPublicKeyRing cert = options.getCertificateSource().getCertificate(keyId); - if (cert != null) { - return cert; - } - - if (options.getMissingCertificateCallback() != null) { - return options.getMissingCertificateCallback().onMissingPublicKeyEncountered(keyId); - } - return null; // TODO: Missing cert for sig - } - - public void updateLiteral(byte b) { - for (OnePassSignatureCheck ops : literalOPS) { - ops.getOnePassSignature().update(b); - } - - for (SignatureCheck detached : detachedSignatures) { - detached.getSignature().update(b); - } - - for (SignatureCheck prepended : prependedSignatures) { - prepended.getSignature().update(b); - } - } - - public void updateLiteral(byte[] b, int off, int len) { - for (OnePassSignatureCheck ops : literalOPS) { - ops.getOnePassSignature().update(b, off, len); - } - - for (SignatureCheck detached : detachedSignatures) { - detached.getSignature().update(b, off, len); - } - - for (SignatureCheck prepended : prependedSignatures) { - prepended.getSignature().update(b, off, len); - } - } - - public void updatePacket(byte b) { - for (int i = opsUpdateStack.size() - 1; i >= 0; i--) { - List nestedOPSs = opsUpdateStack.get(i); - for (OnePassSignatureCheck ops : nestedOPSs) { - ops.getOnePassSignature().update(b); - } - } - } - - public void updatePacket(byte[] buf, int off, int len) { - for (int i = opsUpdateStack.size() - 1; i >= 0; i--) { - List nestedOPSs = opsUpdateStack.get(i); - for (OnePassSignatureCheck ops : nestedOPSs) { - ops.getOnePassSignature().update(buf, off, len); - } - } - } - - public void finish(MessageMetadata.Layer layer, Policy policy) { - for (SignatureCheck detached : detachedSignatures) { - SignatureVerification verification = new SignatureVerification(detached.getSignature(), detached.getSigningKeyIdentifier()); - try { - SignatureValidator.signatureWasCreatedInBounds(options.getVerifyNotBefore(), options.getVerifyNotAfter()) - .verify(detached.getSignature()); - CertificateValidator.validateCertificateAndVerifyInitializedSignature( - detached.getSignature(), KeyRingUtils.publicKeys(detached.getSigningKeyRing()), policy); - LOGGER.debug("Acceptable signature by key " + verification.getSigningKey()); - layer.addVerifiedDetachedSignature(verification); - } catch (SignatureValidationException e) { - LOGGER.debug("Rejected signature by key " + verification.getSigningKey(), e); - layer.addRejectedDetachedSignature(new SignatureVerification.Failure(verification, e)); - } - } - - for (SignatureCheck prepended : prependedSignatures) { - SignatureVerification verification = new SignatureVerification(prepended.getSignature(), prepended.getSigningKeyIdentifier()); - try { - SignatureValidator.signatureWasCreatedInBounds(options.getVerifyNotBefore(), options.getVerifyNotAfter()) - .verify(prepended.getSignature()); - CertificateValidator.validateCertificateAndVerifyInitializedSignature( - prepended.getSignature(), KeyRingUtils.publicKeys(prepended.getSigningKeyRing()), policy); - LOGGER.debug("Acceptable signature by key " + verification.getSigningKey()); - layer.addVerifiedPrependedSignature(verification); - } catch (SignatureValidationException e) { - LOGGER.debug("Rejected signature by key " + verification.getSigningKey(), e); - layer.addRejectedPrependedSignature(new SignatureVerification.Failure(verification, e)); - } - } - - for (SignatureVerification.Failure rejected : inbandSignaturesWithMissingCert) { - layer.addRejectedOnePassSignature(rejected); - } - - for (SignatureVerification.Failure rejected : prependedSignaturesWithMissingCert) { - layer.addRejectedPrependedSignature(rejected); - } - - for (SignatureVerification.Failure rejected : detachedSignaturesWithMissingCert) { - layer.addRejectedDetachedSignature(rejected); - } - } - - @Override - public void write(int b) { - updatePacket((byte) b); - } - - @Override - public void write(@Nonnull byte[] b, int off, int len) { - updatePacket(b, off, len); - } - - public void nextPacket(OpenPgpPacket nextPacket) { - if (nextPacket == OpenPgpPacket.LIT) { - isLiteral = true; - if (literalOPS.isEmpty() && !opsUpdateStack.isEmpty()) { - literalOPS = opsUpdateStack.pop(); - } - } else { - isLiteral = false; - } - } - - } -} 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 be9cf2b1..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMetadata.java +++ /dev/null @@ -1,380 +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.OpenPgpFingerprint; -import org.pgpainless.key.SubkeyIdentifier; -import org.pgpainless.util.SessionKey; - -/** - * Legacy class containing metadata about an OpenPGP message. - * It is advised to use {@link MessageMetadata} instead. - * - * TODO: Remove in 1.5.X - */ -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 SessionKey sessionKey; - private final CompressionAlgorithm compressionAlgorithm; - private final String fileName; - private final Date modificationDate; - private final StreamEncoding fileEncoding; - private final boolean cleartextSigned; - - public OpenPgpMetadata(Set recipientKeyIds, - SubkeyIdentifier decryptionKey, - SessionKey sessionKey, - CompressionAlgorithm algorithm, - List verifiedInbandSignatures, - List invalidInbandSignatures, - List verifiedDetachedSignatures, - List invalidDetachedSignatures, - String fileName, - Date modificationDate, - StreamEncoding fileEncoding, - boolean cleartextSigned) { - - this.recipientKeyIds = Collections.unmodifiableSet(recipientKeyIds); - this.decryptionKey = decryptionKey; - this.sessionKey = sessionKey; - 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; - this.cleartextSigned = cleartextSigned; - } - - /** - * 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 sessionKey != null && sessionKey.getAlgorithm() != SymmetricKeyAlgorithm.NULL; - } - - /** - * 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 (e.g. 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 sessionKey == null ? null : sessionKey.getAlgorithm(); - } - - public @Nullable SessionKey getSessionKey() { - return sessionKey; - } - - /** - * 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) { - OpenPgpFingerprint fingerprint = OpenPgpFingerprint.of(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(OpenPgpFingerprint 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; - } - - /** - * Return true if the message was signed using the cleartext signature framework. - * - * @return true if cleartext signed. - */ - public boolean isCleartextSigned() { - return cleartextSigned; - } - - public static Builder getBuilder() { - return new Builder(); - } - - public static class Builder { - - private final Set recipientFingerprints = new HashSet<>(); - private SessionKey sessionKey; - private SubkeyIdentifier decryptionKey; - private CompressionAlgorithm compressionAlgorithm = CompressionAlgorithm.UNCOMPRESSED; - private String fileName; - private StreamEncoding fileEncoding; - private Date modificationDate; - private boolean cleartextSigned = false; - - 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 setSessionKey(SessionKey sessionKey) { - this.sessionKey = sessionKey; - return this; - } - - public Builder setCompressionAlgorithm(CompressionAlgorithm algorithm) { - this.compressionAlgorithm = algorithm; - return this; - } - - public Builder setFileName(@Nullable 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 Builder addVerifiedInbandSignature(SignatureVerification signatureVerification) { - this.verifiedInbandSignatures.add(signatureVerification); - return this; - } - - public Builder addVerifiedDetachedSignature(SignatureVerification signatureVerification) { - this.verifiedDetachedSignatures.add(signatureVerification); - return this; - } - - public Builder addInvalidInbandSignature(SignatureVerification signatureVerification, SignatureValidationException e) { - this.invalidInbandSignatures.add(new SignatureVerification.Failure(signatureVerification, e)); - return this; - } - - public Builder addInvalidDetachedSignature(SignatureVerification signatureVerification, SignatureValidationException e) { - this.invalidDetachedSignatures.add(new SignatureVerification.Failure(signatureVerification, e)); - return this; - } - - public Builder setCleartextSigned() { - this.cleartextSigned = true; - return this; - } - - public OpenPgpMetadata build() { - return new OpenPgpMetadata( - recipientFingerprints, decryptionKey, - sessionKey, compressionAlgorithm, - verifiedInbandSignatures, invalidInbandSignatures, - verifiedDetachedSignatures, invalidDetachedSignatures, - fileName, modificationDate, fileEncoding, cleartextSigned); - } - } -} 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 4a810eb9..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/SignatureVerification.java +++ /dev/null @@ -1,106 +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.bouncycastle.util.encoders.Hex; -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; - } - - @Override - public String toString() { - return "Signature: " + (signature != null ? Hex.toHexString(signature.getDigestPrefix()) : "null") - + "; Key: " + (signingKey != null ? signingKey.toString() : "null") + ";"; - } - - /** - * 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; - } - - @Override - public String toString() { - return signatureVerification.toString() + " Failure: " + getValidationException().getMessage(); - } - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java deleted file mode 100644 index 28b415e0..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java +++ /dev/null @@ -1,164 +0,0 @@ -// SPDX-FileCopyrightText: 2022 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 java.util.NoSuchElementException; - -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.PGPException; -import org.bouncycastle.openpgp.PGPLiteralData; -import org.bouncycastle.openpgp.PGPOnePassSignature; -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 {@link BCPGInputStream#read()} to tee the data out though, since - * {@link BCPGInputStream#readPacket()} inconsistently calls a mix of {@link BCPGInputStream#read()} and - * {@link 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 {@link DelayedTeeInputStream} which wraps the underlying - * stream. Since calling {@link 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 {@link TeeBCPGInputStream} handle pushing this byte to the output stream using - * {@link DelayedTeeInputStream#squeeze()}. - */ -public class TeeBCPGInputStream { - - protected final DelayedTeeInputStream delayedTee; - // InputStream of OpenPGP packets of the current layer - protected final BCPGInputStream packetInputStream; - - public TeeBCPGInputStream(BCPGInputStream inputStream, OutputStream outputStream) { - this.delayedTee = new DelayedTeeInputStream(inputStream, outputStream); - this.packetInputStream = BCPGInputStream.wrap(delayedTee); - } - - public OpenPgpPacket nextPacketTag() throws IOException { - int tag = packetInputStream.nextPacketTag(); - if (tag == -1) { - return null; - } - - OpenPgpPacket packet; - try { - packet = OpenPgpPacket.requireFromTag(tag); - } catch (NoSuchElementException e) { - throw e; - } - return packet; - } - - public Packet readPacket() throws IOException { - return packetInputStream.readPacket(); - } - - public PGPCompressedData readCompressedData() throws IOException { - delayedTee.squeeze(); - PGPCompressedData compressedData = new PGPCompressedData(packetInputStream); - return compressedData; - } - - public PGPLiteralData readLiteralData() throws IOException { - delayedTee.squeeze(); - return new PGPLiteralData(packetInputStream); - } - - public PGPEncryptedDataList readEncryptedDataList() throws IOException { - delayedTee.squeeze(); - return new PGPEncryptedDataList(packetInputStream); - } - - public PGPOnePassSignature readOnePassSignature() throws PGPException, IOException { - PGPOnePassSignature onePassSignature = new PGPOnePassSignature(packetInputStream); - delayedTee.squeeze(); - return onePassSignature; - } - - public PGPSignature readSignature() throws PGPException, IOException { - PGPSignature signature = new PGPSignature(packetInputStream); - delayedTee.squeeze(); - return signature; - } - - public MarkerPacket readMarker() throws IOException { - MarkerPacket markerPacket = (MarkerPacket) readPacket(); - delayedTee.squeeze(); - return markerPacket; - } - - public void close() throws IOException { - this.packetInputStream.close(); - } - - public static class DelayedTeeInputStream extends InputStream { - - private int last = -1; - private final InputStream inputStream; - private final OutputStream outputStream; - - public DelayedTeeInputStream(InputStream inputStream, OutputStream outputStream) { - this.inputStream = inputStream; - this.outputStream = outputStream; - } - - @Override - public int read() throws IOException { - if (last != -1) { - outputStream.write(last); - } - try { - last = inputStream.read(); - return last; - } catch (IOException e) { - if ("crc check failed in armored message.".equals(e.getMessage())) { - throw e; - } - return -1; - } - } - - @Override - public int read(byte[] b, int off, int len) throws IOException { - if (last != -1) { - outputStream.write(last); - } - - int r = inputStream.read(b, off, len); - if (r > 0) { - outputStream.write(b, off, r - 1); - last = b[off + r - 1]; - } else { - last = -1; - } - return r; - } - - /** - * Squeeze the last byte out and update the output stream. - * - * @throws IOException in case of an IO error - */ - public void squeeze() throws IOException { - if (last != -1) { - outputStream.write(last); - } - last = -1; - } - - @Override - public void close() throws IOException { - inputStream.close(); - outputStream.close(); - } - } -} 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 cd0f6b35..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/ClearsignedMessageUtil.java +++ /dev/null @@ -1,169 +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 - * @throws WrongConsumingMethodException in case the armored message is not cleartext signed - */ - public static PGPSignatureList detachSignaturesFromInbandClearsignedMessage(InputStream clearsignedInputStream, - OutputStream messageOutputStream) - throws IOException, WrongConsumingMethodException { - ArmoredInputStream in; - if (clearsignedInputStream instanceof ArmoredInputStream) { - in = (ArmoredInputStream) clearsignedInputStream; - } else { - 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 = ImplementationFactory.getInstance().getPGPObjectFactory(in); - 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/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 5aa9f548..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 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 {@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 (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 - */ - 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/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/decryption_verification/syntax_check/InputSymbol.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/InputSymbol.java deleted file mode 100644 index 854c3305..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/InputSymbol.java +++ /dev/null @@ -1,45 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification.syntax_check; - -import org.bouncycastle.openpgp.PGPCompressedData; -import org.bouncycastle.openpgp.PGPEncryptedDataList; -import org.bouncycastle.openpgp.PGPLiteralData; -import org.bouncycastle.openpgp.PGPOnePassSignatureList; -import org.bouncycastle.openpgp.PGPSignatureList; - -public enum InputSymbol { - /** - * A {@link PGPLiteralData} packet. - */ - LiteralData, - /** - * A {@link PGPSignatureList} object. - */ - Signature, - /** - * A {@link PGPOnePassSignatureList} object. - */ - OnePassSignature, - /** - * A {@link 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. - */ - CompressedData, - /** - * A {@link PGPEncryptedDataList} object. - * This object combines multiple ESKs and the corresponding Symmetrically Encrypted - * (possibly Integrity Protected) Data packet. - */ - EncryptedData, - /** - * 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). - */ - EndOfSequence -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.java deleted file mode 100644 index 6abb507a..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.java +++ /dev/null @@ -1,138 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification.syntax_check; - -import org.pgpainless.exception.MalformedOpenPgpMessageException; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * This class describes the syntax for OpenPGP messages as specified by rfc4880. - * - * @see - * rfc4880 - §11.3. OpenPGP Messages - * @see - * Blog post about theoretic background and translation of grammar to PDA syntax - * @see - * Blog post about practically implementing the PDA for packet syntax validation - */ -public class OpenPgpMessageSyntax implements Syntax { - - @Override - public @Nonnull Transition transition(@Nonnull State from, @Nonnull InputSymbol input, @Nullable StackSymbol stackItem) - throws MalformedOpenPgpMessageException { - switch (from) { - case OpenPgpMessage: - return fromOpenPgpMessage(input, stackItem); - case LiteralMessage: - return fromLiteralMessage(input, stackItem); - case CompressedMessage: - return fromCompressedMessage(input, stackItem); - case EncryptedMessage: - return fromEncryptedMessage(input, stackItem); - case Valid: - return fromValid(input, stackItem); - } - - throw new MalformedOpenPgpMessageException(from, input, stackItem); - } - - @Nonnull - Transition fromOpenPgpMessage(@Nonnull InputSymbol input, @Nullable StackSymbol stackItem) - throws MalformedOpenPgpMessageException { - if (stackItem != StackSymbol.msg) { - throw new MalformedOpenPgpMessageException(State.OpenPgpMessage, input, stackItem); - } - - switch (input) { - case LiteralData: - return new Transition(State.LiteralMessage); - - case Signature: - return new Transition(State.OpenPgpMessage, StackSymbol.msg); - - case OnePassSignature: - return new Transition(State.OpenPgpMessage, StackSymbol.ops, StackSymbol.msg); - - case CompressedData: - return new Transition(State.CompressedMessage); - - case EncryptedData: - return new Transition(State.EncryptedMessage); - - case EndOfSequence: - default: - throw new MalformedOpenPgpMessageException(State.OpenPgpMessage, input, stackItem); - } - } - - @Nonnull - Transition fromLiteralMessage(@Nonnull InputSymbol input, @Nullable StackSymbol stackItem) - throws MalformedOpenPgpMessageException { - switch (input) { - case Signature: - if (stackItem == StackSymbol.ops) { - return new Transition(State.LiteralMessage); - } - break; - - case EndOfSequence: - if (stackItem == StackSymbol.terminus) { - return new Transition(State.Valid); - } - break; - } - - throw new MalformedOpenPgpMessageException(State.LiteralMessage, input, stackItem); - } - - @Nonnull - Transition fromCompressedMessage(@Nonnull InputSymbol input, @Nullable StackSymbol stackItem) - throws MalformedOpenPgpMessageException { - switch (input) { - case Signature: - if (stackItem == StackSymbol.ops) { - return new Transition(State.CompressedMessage); - } - break; - - case EndOfSequence: - if (stackItem == StackSymbol.terminus) { - return new Transition(State.Valid); - } - break; - } - - throw new MalformedOpenPgpMessageException(State.CompressedMessage, input, stackItem); - } - - @Nonnull - Transition fromEncryptedMessage(@Nonnull InputSymbol input, @Nullable StackSymbol stackItem) - throws MalformedOpenPgpMessageException { - switch (input) { - case Signature: - if (stackItem == StackSymbol.ops) { - return new Transition(State.EncryptedMessage); - } - break; - - case EndOfSequence: - if (stackItem == StackSymbol.terminus) { - return new Transition(State.Valid); - } - break; - } - - throw new MalformedOpenPgpMessageException(State.EncryptedMessage, input, stackItem); - } - - @Nonnull - Transition fromValid(@Nonnull InputSymbol input, @Nullable StackSymbol stackItem) - throws MalformedOpenPgpMessageException { - // There is no applicable transition rule out of Valid - throw new MalformedOpenPgpMessageException(State.Valid, input, stackItem); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java deleted file mode 100644 index 7d7cf973..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java +++ /dev/null @@ -1,156 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification.syntax_check; - -import org.pgpainless.exception.MalformedOpenPgpMessageException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Stack; - -import static org.pgpainless.decryption_verification.syntax_check.StackSymbol.msg; -import static org.pgpainless.decryption_verification.syntax_check.StackSymbol.terminus; - -/** - * 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 - */ -public class PDA { - - private static final Logger LOGGER = LoggerFactory.getLogger(PDA.class); - - // right now we implement what rfc4880 specifies. - // TODO: Consider implementing what we proposed here: - // https://mailarchive.ietf.org/arch/msg/openpgp/uepOF6XpSegMO4c59tt9e5H1i4g/ - private final Syntax syntax; - private final Stack stack = new Stack<>(); - private final List inputs = new ArrayList<>(); // Track inputs for debugging / error reporting - private State state; - - /** - * Default constructor which initializes the PDA to work with the {@link OpenPgpMessageSyntax}. - */ - public PDA() { - this(new OpenPgpMessageSyntax(), State.OpenPgpMessage, terminus, msg); - } - - /** - * Construct a PDA with a custom {@link Syntax}, initial {@link State} and initial {@link StackSymbol StackSymbols}. - * - * @param syntax syntax - * @param initialState initial state - * @param initialStack zero or more initial stack items (get pushed onto the stack in order of appearance) - */ - public PDA(@Nonnull Syntax syntax, @Nonnull State initialState, @Nonnull StackSymbol... initialStack) { - this.syntax = syntax; - this.state = initialState; - for (StackSymbol symbol : initialStack) { - pushStack(symbol); - } - } - - /** - * Process the next {@link InputSymbol}. - * This will either leave the PDA in the next state, or throw a {@link MalformedOpenPgpMessageException} if the - * input symbol is rejected. - * - * @param input input symbol - * @throws MalformedOpenPgpMessageException if the input symbol is rejected - */ - public void next(@Nonnull InputSymbol input) - throws MalformedOpenPgpMessageException { - StackSymbol stackSymbol = popStack(); - try { - Transition transition = syntax.transition(state, input, stackSymbol); - state = transition.getNewState(); - for (StackSymbol item : transition.getPushedItems()) { - pushStack(item); - } - inputs.add(input); - } catch (MalformedOpenPgpMessageException e) { - MalformedOpenPgpMessageException wrapped = new MalformedOpenPgpMessageException( - "Malformed message: After reading packet sequence " + Arrays.toString(inputs.toArray()) + - ", token '" + input + "' is not allowed." + - "\nNo transition from state '" + state + "' with stack " + Arrays.toString(stack.toArray()) + - (stackSymbol != null ? "||'" + stackSymbol + "'." : "."), e); - LOGGER.debug("Invalid input '" + input + "'", wrapped); - throw wrapped; - } - } - - /** - * Return the current state of the PDA. - * - * @return state - */ - public @Nonnull State getState() { - return state; - } - - /** - * Peek at the stack, returning the topmost stack item without changing the stack. - * - * @return topmost stack item, or null if stack is empty - */ - public @Nullable StackSymbol peekStack() { - if (stack.isEmpty()) { - return null; - } - return stack.peek(); - } - - /** - * Return true, if the PDA is in a valid state (the OpenPGP message is valid). - * - * @return true if valid, false otherwise - */ - public boolean isValid() { - return getState() == State.Valid && stack.isEmpty(); - } - - /** - * Throw a {@link MalformedOpenPgpMessageException} if the pda is not in a valid state right now. - * - * @throws MalformedOpenPgpMessageException if the pda is not in an acceptable state - */ - public void assertValid() throws MalformedOpenPgpMessageException { - if (!isValid()) { - throw new MalformedOpenPgpMessageException("Pushdown Automaton is not in an acceptable state: " + toString()); - } - } - - /** - * Pop an item from the stack. - * - * @return stack item - */ - private StackSymbol popStack() { - if (stack.isEmpty()) { - return null; - } - return stack.pop(); - } - - /** - * Push an item onto the stack. - * - * @param item item - */ - private void pushStack(StackSymbol item) { - stack.push(item); - } - - @Override - public String toString() { - return "State: " + state + " Stack: " + stack; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/StackSymbol.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/StackSymbol.java deleted file mode 100644 index 120458e5..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/StackSymbol.java +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification.syntax_check; - -public enum 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/java/org/pgpainless/decryption_verification/syntax_check/State.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/State.java deleted file mode 100644 index 9dee9af1..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/State.java +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification.syntax_check; - -/** - * Set of states of the automaton. - */ -public enum State { - OpenPgpMessage, - LiteralMessage, - CompressedMessage, - EncryptedMessage, - Valid -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Syntax.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Syntax.java deleted file mode 100644 index 2f3d0a57..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Syntax.java +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification.syntax_check; - -import org.pgpainless.exception.MalformedOpenPgpMessageException; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * This interface can be used to define a custom syntax for the {@link PDA}. - */ -public interface Syntax { - - /** - * Describe a transition rule from {@link State}
from
for {@link InputSymbol}
input
- * with {@link StackSymbol}
stackItem
from the top of the {@link PDA PDAs} stack. - * The resulting {@link Transition} contains the new {@link State}, as well as a list of - * {@link StackSymbol StackSymbols} that get pushed onto the stack by the transition rule. - * If there is no applicable rule, a {@link MalformedOpenPgpMessageException} is thrown, since in this case - * the {@link 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) - */ - @Nonnull Transition transition(@Nonnull State from, @Nonnull InputSymbol input, @Nullable StackSymbol stackItem) - throws MalformedOpenPgpMessageException; -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Transition.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Transition.java deleted file mode 100644 index ab0db5ef..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Transition.java +++ /dev/null @@ -1,48 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification.syntax_check; - -import javax.annotation.Nonnull; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -/** - * Result of applying a transition rule. - * Transition rules can be described by implementing the {@link Syntax} interface. - */ -public class Transition { - - private final List pushedItems = new ArrayList<>(); - private final State newState; - - public Transition(@Nonnull State newState, @Nonnull StackSymbol... pushedItems) { - this.newState = newState; - this.pushedItems.addAll(Arrays.asList(pushedItems)); - } - - /** - * Return the new {@link State} that is reached by applying the transition. - * - * @return new state - */ - @Nonnull - public State getNewState() { - return newState; - } - - /** - * Return a list of {@link StackSymbol StackSymbols} 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. - * - * @return list of items to be pushed onto the stack - */ - @Nonnull - public List getPushedItems() { - return new ArrayList<>(pushedItems); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/package-info.java deleted file mode 100644 index 4df6af5a..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Pushdown Automaton to verify validity of packet sequences according to the OpenPGP Message format. - */ -package org.pgpainless.decryption_verification.syntax_check; diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/BcHashContextSigner.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/BcHashContextSigner.java deleted file mode 100644 index 54c6df9e..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/BcHashContextSigner.java +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.encryption_signing; - -import java.security.MessageDigest; -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.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.PGPSignatureGenerator; -import org.pgpainless.PGPainless; -import org.pgpainless.algorithm.SignatureType; -import org.pgpainless.key.info.KeyRingInfo; -import org.pgpainless.key.protection.SecretKeyRingProtector; -import org.pgpainless.key.protection.UnlockSecretKey; - -import javax.annotation.Nonnull; - -public class BcHashContextSigner { - - public static PGPSignature signHashContext(@Nonnull MessageDigest hashContext, - @Nonnull SignatureType signatureType, - @Nonnull PGPSecretKeyRing secretKeys, - @Nonnull SecretKeyRingProtector protector) - throws PGPException { - KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); - List signingSubkeyCandidates = info.getSigningSubkeys(); - PGPSecretKey signingKey = null; - for (PGPPublicKey signingKeyCandidate : signingSubkeyCandidates) { - signingKey = secretKeys.getSecretKey(signingKeyCandidate.getKeyID()); - if (signingKey != null) { - break; - } - } - if (signingKey == null) { - throw new PGPException("Key does not contain suitable signing subkey."); - } - - PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(signingKey, protector); - return signHashContext(hashContext, signatureType, privateKey); - } - - /** - * Create an OpenPGP Signature over the given {@link MessageDigest} hash context. - * - * @param hashContext hash context - * @param privateKey signing-capable key - * @return signature - * @throws PGPException in case of an OpenPGP error - */ - static PGPSignature signHashContext(MessageDigest hashContext, SignatureType signatureType, PGPPrivateKey privateKey) - throws PGPException { - PGPSignatureGenerator sigGen = new PGPSignatureGenerator( - new BcPGPHashContextContentSignerBuilder(hashContext) - ); - - sigGen.init(signatureType.getCode(), privateKey); - return sigGen.generate(); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/BcPGPHashContextContentSignerBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/BcPGPHashContextContentSignerBuilder.java deleted file mode 100644 index df6f6ca3..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/BcPGPHashContextContentSignerBuilder.java +++ /dev/null @@ -1,169 +0,0 @@ -// SPDX-FileCopyrightText: 2022 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.DataLengthException; -import org.bouncycastle.crypto.Digest; -import org.bouncycastle.crypto.Signer; -import org.bouncycastle.crypto.params.AsymmetricKeyParameter; -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.PGPContentSignerBuilder; -import org.bouncycastle.openpgp.operator.bc.BcPGPKeyConverter; -import org.bouncycastle.util.Arrays; -import org.pgpainless.algorithm.HashAlgorithm; -import org.pgpainless.algorithm.PublicKeyAlgorithm; - -/** - * Implementation of {@link 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 extends PGPHashContextContentSignerBuilder { - - private final BcPGPKeyConverter keyConverter = new BcPGPKeyConverter(); - private final MessageDigest messageDigest; - private final HashAlgorithm hashAlgorithm; - - BcPGPHashContextContentSignerBuilder(MessageDigest messageDigest) { - this.messageDigest = messageDigest; - this.hashAlgorithm = HashAlgorithm.fromName(messageDigest.getAlgorithm()); - if (hashAlgorithm == null) { - throw new IllegalArgumentException("Cannot recognize OpenPGP Hash Algorithm: " + messageDigest.getAlgorithm()); - } - } - - @Override - public PGPContentSigner build(int signatureType, PGPPrivateKey privateKey) throws PGPException { - PublicKeyAlgorithm keyAlgorithm = PublicKeyAlgorithm.requireFromId(privateKey.getPublicKeyPacket().getAlgorithm()); - AsymmetricKeyParameter privKeyParam = keyConverter.getPrivateKey(privateKey); - final Signer signer = createSigner(keyAlgorithm, messageDigest, privKeyParam); - signer.init(true, privKeyParam); - - return new PGPContentSigner() { - public int getType() { - return signatureType; - } - - public int getHashAlgorithm() { - return hashAlgorithm.getAlgorithmId(); - } - - public int getKeyAlgorithm() { - return keyAlgorithm.getAlgorithmId(); - } - - public long getKeyID() { - return privateKey.getKeyID(); - } - - public OutputStream getOutputStream() { - return new PGPHashContextContentSignerBuilder.SignerOutputStream(signer); - } - - public byte[] getSignature() { - try { - return signer.generateSignature(); - } catch (CryptoException e) { - throw new IllegalStateException("unable to create signature"); - } - } - - public byte[] getDigest() { - return messageDigest.digest(); - } - }; - } - - static Signer createSigner( - PublicKeyAlgorithm keyAlgorithm, - MessageDigest messageDigest, - CipherParameters keyParam) - throws PGPException { - ExistingMessageDigest staticDigest = new ExistingMessageDigest(messageDigest); - switch (keyAlgorithm.getAlgorithmId()) { - case PublicKeyAlgorithmTags.RSA_GENERAL: - case PublicKeyAlgorithmTags.RSA_SIGN: - return new RSADigestSigner(staticDigest); - case PublicKeyAlgorithmTags.DSA: - return new DSADigestSigner(new DSASigner(), staticDigest); - case PublicKeyAlgorithmTags.ECDSA: - return new DSADigestSigner(new ECDSASigner(), staticDigest); - case PublicKeyAlgorithmTags.EDDSA: - if (keyParam instanceof Ed25519PrivateKeyParameters || keyParam instanceof Ed25519PublicKeyParameters) { - return new EdDsaSigner(new Ed25519Signer(), staticDigest); - } - return new EdDsaSigner(new Ed448Signer(new byte[0]), staticDigest); - default: - throw new PGPException("cannot recognise keyAlgorithm: " + keyAlgorithm); - } - } - - // Copied from BCs BcImplProvider - required since BCs class is package visible only :/ - private static class EdDsaSigner - implements Signer { - private final Signer signer; - private final Digest digest; - private final byte[] digBuf; - - EdDsaSigner(Signer signer, Digest digest) { - this.signer = signer; - this.digest = digest; - this.digBuf = new byte[digest.getDigestSize()]; - } - - public void init(boolean forSigning, CipherParameters param) { - this.signer.init(forSigning, param); - this.digest.reset(); - } - - public void update(byte b) { - this.digest.update(b); - } - - public void update(byte[] in, int off, int len) { - this.digest.update(in, off, len); - } - - public byte[] generateSignature() - throws CryptoException, DataLengthException { - digest.doFinal(digBuf, 0); - - signer.update(digBuf, 0, digBuf.length); - - return signer.generateSignature(); - } - - public boolean verifySignature(byte[] signature) { - digest.doFinal(digBuf, 0); - - signer.update(digBuf, 0, digBuf.length); - - return signer.verifySignature(signature); - } - - public void reset() { - Arrays.clear(digBuf); - signer.reset(); - digest.reset(); - } - } - -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/CRLFGeneratorStream.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/CRLFGeneratorStream.java deleted file mode 100644 index 4cab8be3..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/CRLFGeneratorStream.java +++ /dev/null @@ -1,55 +0,0 @@ -// SPDX-FileCopyrightText: 2021 David Hook -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.encryption_signing; - -import org.pgpainless.algorithm.StreamEncoding; - -import java.io.IOException; -import java.io.OutputStream; - -/** - * {@link OutputStream} which applies CR-LF encoding of its input data, based on the desired {@link StreamEncoding}. - * This implementation originates from the Bouncy Castle library. - */ -public class CRLFGeneratorStream extends OutputStream { - - protected final OutputStream crlfOut; - private final boolean isBinary; - private int lastB = 0; - - public CRLFGeneratorStream(OutputStream crlfOut, StreamEncoding encoding) { - this.crlfOut = crlfOut; - this.isBinary = encoding == StreamEncoding.BINARY; - } - - public void write(int b) throws IOException { - if (!isBinary) { - if (b == '\n' && lastB != '\r') { // Unix - crlfOut.write('\r'); - } else if (lastB == '\r') { // MAC - if (b != '\n') { - crlfOut.write('\n'); - } - } - lastB = b; - } - - crlfOut.write(b); - } - - public void close() throws IOException { - if (!isBinary && lastB == '\r') { // MAC - crlfOut.write('\n'); - } - crlfOut.close(); - } - - @Override - public void flush() throws IOException { - super.flush(); - crlfOut.flush(); - } -} 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 c705c0b1..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionBuilderInterface.java +++ /dev/null @@ -1,38 +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 stream - * - * @throws PGPException if something goes wrong during encryption stream preparation - * @throws IOException if something goes wrong during encryption stream preparation (writing headers) - */ - 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 b5baee83..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java +++ /dev/null @@ -1,357 +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.NoSuchElementException; -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.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; - -/** - * 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 encrypt to all suitable, encryption capable subkeys on each recipient's certificate. - * This behavior can be changed per recipient, e.g. by calling - *
- * {@code
- * opt.addRecipient(aliceKey, EncryptionOptions.encryptToFirstSubkey());
- * }
- * 
- * 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.ANY); - } - - public EncryptionOptions(EncryptionPurpose purpose) { - this.purpose = purpose; - } - - /** - * Factory method to create an {@link EncryptionOptions} object which will encrypt for keys - * which carry either the {@link org.pgpainless.algorithm.KeyFlag#ENCRYPT_COMMS} or - * {@link org.pgpainless.algorithm.KeyFlag#ENCRYPT_STORAGE} flag. - * - * Use this if you are not sure. - * - * @return encryption options - */ - public static EncryptionOptions get() { - return new EncryptionOptions(); - } - - /** - * 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 {@link Iterable} (e.g. {@link PGPPublicKeyRingCollection}) as recipients. - * - * @param keys keys - * @return this - */ - public EncryptionOptions addRecipients(Iterable keys) { - if (!keys.iterator().hasNext()) { - throw new IllegalArgumentException("Set of recipient keys cannot be empty."); - } - for (PGPPublicKeyRing key : keys) { - addRecipient(key); - } - return this; - } - - /** - * Add all key rings in the provided {@link Iterable} (e.g. {@link 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 - */ - public EncryptionOptions addRecipients(@Nonnull Iterable keys, @Nonnull EncryptionKeySelector selector) { - if (!keys.iterator().hasNext()) { - throw new IllegalArgumentException("Set of recipient keys cannot be empty."); - } - 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 KeyException.UnacceptableEncryptionKeyException(OpenPgpFingerprint.of(key)); - } - - 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) { - Date evaluationDate = new Date(); - KeyRingInfo info; - info = new KeyRingInfo(key, evaluationDate); - - Date primaryKeyExpiration; - try { - primaryKeyExpiration = info.getPrimaryKeyExpirationDate(); - } catch (NoSuchElementException e) { - throw new KeyException.UnacceptableSelfSignatureException(OpenPgpFingerprint.of(key)); - } - if (primaryKeyExpiration != null && primaryKeyExpiration.before(evaluationDate)) { - throw new KeyException.ExpiredKeyException(OpenPgpFingerprint.of(key), primaryKeyExpiration); - } - - List encryptionSubkeys = encryptionKeySelectionStrategy - .selectEncryptionSubkeys(info.getEncryptionSubkeys(purpose)); - if (encryptionSubkeys.isEmpty()) { - throw new KeyException.UnacceptableEncryptionKeyException(OpenPgpFingerprint.of(key)); - } - - 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 - * @return this - */ - public EncryptionOptions overrideEncryptionAlgorithm(SymmetricKeyAlgorithm encryptionAlgorithm) { - if (encryptionAlgorithm == SymmetricKeyAlgorithm.NULL) { - throw new IllegalArgumentException("Plaintext encryption can only be used to denote unencrypted secret keys."); - } - this.encryptionAlgorithmOverride = encryptionAlgorithm; - return this; - } - - /** - * Return
true
iff the user specified at least one encryption method, - *
false
otherwise. - * - * @return encryption methods is not empty - */ - public boolean hasEncryptionMethod() { - return !encryptionMethods.isEmpty(); - } - - 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 4112092c..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionResult.java +++ /dev/null @@ -1,209 +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.PGPPublicKeyRing; -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 - * */ - 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()); - } - - /** - * 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. - */ - public boolean isEncryptedFor(PGPPublicKeyRing certificate) { - for (SubkeyIdentifier recipient : recipients) { - if (certificate.getPublicKey().getKeyID() != recipient.getPrimaryKeyId()) { - continue; - } - - if (certificate.getPublicKey(recipient.getSubkeyId()) != null) { - return true; - } - } - return false; - } - - /** - * 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 final 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 2e370b94..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java +++ /dev/null @@ -1,322 +0,0 @@ -// SPDX-FileCopyrightText: 2018 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 java.util.ArrayList; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Set; -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.HashAlgorithm; -import org.pgpainless.algorithm.StreamEncoding; -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; -import org.pgpainless.implementation.ImplementationFactory; -import org.pgpainless.key.SubkeyIdentifier; -import org.pgpainless.util.ArmorUtils; -import org.pgpainless.util.ArmoredOutputStreamFactory; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * 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 - */ -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; - OutputStream signatureLayerStream; - - 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(); - prepareSigningStream(); - prepareInputEncoding(); - } - - private void prepareArmor() { - if (!options.isAsciiArmor()) { - LOGGER.debug("Output will be unarmored"); - return; - } - - // ArmoredOutputStream better be buffered - outermostStream = new BufferedOutputStream(outermostStream); - - LOGGER.debug("Wrap encryption output in ASCII armor"); - armorOutputStream = ArmoredOutputStreamFactory.get(outermostStream, options); - if (options.hasComment()) { - String[] commentLines = options.getComment().split("\n"); - for (String commentLine : commentLines) { - if (!commentLine.trim().isEmpty()) { - ArmorUtils.addCommentHeader(armorOutputStream, commentLine.trim()); - } - } - } - 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 { - signatureLayerStream = outermostStream; - 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()) { - int[] algorithmIds = collectHashAlgorithmsForCleartextSigning(); - armorOutputStream.beginClearText(algorithmIds); - 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()); - } - - public void prepareSigningStream() { - outermostStream = new SignatureGenerationStream(outermostStream, options.getSigningOptions()); - } - - public void prepareInputEncoding() { - // 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 bufferedOutputStream = new BufferedOutputStream(outermostStream); - CRLFGeneratorStream crlfGeneratorStream = new CRLFGeneratorStream(bufferedOutputStream, - options.isApplyCRLFEncoding() ? StreamEncoding.UTF8 : StreamEncoding.BINARY); - outermostStream = crlfGeneratorStream; - } - - private int[] collectHashAlgorithmsForCleartextSigning() { - SigningOptions signOpts = options.getSigningOptions(); - Set hashAlgorithms = new HashSet<>(); - if (signOpts != null) { - for (SigningOptions.SigningMethod method : signOpts.getSigningMethods().values()) { - hashAlgorithms.add(method.getHashAlgorithm()); - } - } - - int[] algorithmIds = new int[hashAlgorithms.size()]; - Iterator iterator = hashAlgorithms.iterator(); - for (int i = 0; i < algorithmIds.length; i++) { - algorithmIds[i] = iterator.next().getAlgorithmId(); - } - - return algorithmIds; - } - - @Override - public void write(int data) throws IOException { - outermostStream.write(data); - } - - @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); - } - - @Override - public void flush() throws IOException { - outermostStream.flush(); - } - - @Override - public void close() throws IOException { - if (closed) { - return; - } - - outermostStream.close(); - - // 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<>(signingOptions.getSigningMethods().keySet()); - 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(signatureLayerStream); - } - } - } - - 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/PGPHashContextContentSignerBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/PGPHashContextContentSignerBuilder.java deleted file mode 100644 index 2ea36206..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/PGPHashContextContentSignerBuilder.java +++ /dev/null @@ -1,86 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.encryption_signing; - -import java.io.IOException; -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 implements PGPContentSignerBuilder { - - // Copied from BC, required since BCs class is package visible only - static class SignerOutputStream - extends OutputStream { - private Signer sig; - - SignerOutputStream(Signer sig) { - this.sig = sig; - } - - public void write(byte[] bytes, int off, int len) - throws IOException { - sig.update(bytes, off, len); - } - - public void write(byte[] bytes) - throws IOException { - sig.update(bytes, 0, bytes.length); - } - - public void write(int b) - throws IOException { - sig.update((byte) b); - } - } - - - static class ExistingMessageDigest implements Digest { - - private final MessageDigest digest; - - ExistingMessageDigest(MessageDigest messageDigest) { - this.digest = messageDigest; - } - - @Override - public void update(byte in) { - digest.update(in); - } - - @Override - public void update(byte[] in, int inOff, int len) { - digest.update(in, inOff, len); - } - - @Override - public int doFinal(byte[] out, int outOff) { - byte[] hash = digest.digest(); - System.arraycopy(hash, 0, out, outOff, hash.length); - return getDigestSize(); - } - - @Override - public void 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. - } - - @Override - public String getAlgorithmName() { - return digest.getAlgorithm(); - } - - @Override - public int getDigestSize() { - return digest.getDigestLength(); - } - } - -} 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 4948a7fe..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/ProducerOptions.java +++ /dev/null @@ -1,324 +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 encodingField = StreamEncoding.BINARY; - private boolean applyCRLFEncoding = false; - private boolean cleartextSigned = false; - private boolean hideArmorHeaders = false; - - private CompressionAlgorithm compressionAlgorithmOverride = PGPainless.getPolicy().getCompressionAlgorithmPolicy() - .defaultCompressionAlgorithm(); - private boolean asciiArmor = true; - private String comment = null; - - 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); - } - - /** - * Only wrap the data in an OpenPGP packet. - * No encryption or signing will be applied. - * - * @return builder - */ - 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 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; - } - - /** - * 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 {@link org.pgpainless.util.ArmoredOutputStreamFactory#setComment(String)}, - * then both comments will be written to the produced ASCII armor. - * - * @param comment comment header text - * @return builder - */ - public ProducerOptions setComment(String comment) { - if (!asciiArmor) { - throw new IllegalArgumentException("Comment can only be set when ASCII armoring is enabled."); - } - this.comment = comment; - return this; - } - - /** - * Return comment set for header in ascii armored output. - * - * @return comment - */ - public String getComment() { - return comment; - } - - /** - * Return whether a comment was set (!= null). - * - * @return comment - */ - public boolean hasComment() { - return comment != null; - } - - 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 - * @deprecated deprecated since at least crypto-refresh-05. It is not recommended using this special filename in - * newly generated literal data packets - */ - @Deprecated - 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 format metadata field of the literal data packet. - * Defaults to {@link 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 {@link #applyCRLFEncoding()} instead. - * - * @see RFC4880 §5.9. Literal Data Packet - * - * @param encoding encoding - * @return this - * - * @deprecated options other than the default value of {@link StreamEncoding#BINARY} are discouraged. - */ - @Deprecated - public ProducerOptions setEncoding(@Nonnull StreamEncoding encoding) { - this.encodingField = encoding; - return this; - } - - public StreamEncoding getEncoding() { - return 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 - */ - public ProducerOptions applyCRLFEncoding() { - this.applyCRLFEncoding = true; - return this; - } - - /** - * Return the input encoding that will be applied before signing / encryption. - * - * @return input encoding - */ - public boolean isApplyCRLFEncoding() { - return applyCRLFEncoding; - } - - /** - * 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; - } - - public boolean isHideArmorHeaders() { - return 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 {@link #setComment(String)}, those are not omitted, even if - * {@link #hideArmorHeaders} is set to
true
. - * - * @param hideArmorHeaders true or false - * @return this - */ - public ProducerOptions setHideArmorHeaders(boolean hideArmorHeaders) { - this.hideArmorHeaders = hideArmorHeaders; - return this; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SignatureGenerationStream.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SignatureGenerationStream.java deleted file mode 100644 index 7a96f2e1..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SignatureGenerationStream.java +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.encryption_signing; - -import org.bouncycastle.openpgp.PGPSignatureGenerator; -import org.pgpainless.key.SubkeyIdentifier; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.io.IOException; -import java.io.OutputStream; - -/** - * OutputStream which has the task of updating signature generators for written data. - */ -class SignatureGenerationStream extends OutputStream { - - private final OutputStream wrapped; - private final SigningOptions options; - - SignatureGenerationStream(@Nonnull OutputStream wrapped, @Nullable SigningOptions signingOptions) { - this.wrapped = wrapped; - this.options = signingOptions; - } - - @Override - public void write(int b) throws IOException { - wrapped.write(b); - if (options == null || options.getSigningMethods().isEmpty()) { - return; - } - - for (SubkeyIdentifier signingKey : options.getSigningMethods().keySet()) { - SigningOptions.SigningMethod signingMethod = options.getSigningMethods().get(signingKey); - PGPSignatureGenerator signatureGenerator = signingMethod.getSignatureGenerator(); - byte asByte = (byte) (b & 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 { - wrapped.write(buffer, 0, len); - if (options == null || options.getSigningMethods().isEmpty()) { - return; - } - for (SubkeyIdentifier signingKey : options.getSigningMethods().keySet()) { - SigningOptions.SigningMethod signingMethod = options.getSigningMethods().get(signingKey); - PGPSignatureGenerator signatureGenerator = signingMethod.getSignatureGenerator(); - signatureGenerator.update(buffer, 0, len); - } - } - - @Override - public void close() throws IOException { - wrapped.close(); - } -} 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 0af07fc9..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java +++ /dev/null @@ -1,475 +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 javax.annotation.Nullable; - -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.PGPSignatureGenerator; -import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder; -import org.pgpainless.PGPainless; -import org.pgpainless.algorithm.DocumentSignatureType; -import org.pgpainless.algorithm.HashAlgorithm; -import org.pgpainless.algorithm.PublicKeyAlgorithm; -import org.pgpainless.algorithm.negotiation.HashAlgorithmNegotiator; -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.KeyRingInfo; -import org.pgpainless.key.protection.SecretKeyRingProtector; -import org.pgpainless.key.protection.UnlockSecretKey; -import org.pgpainless.policy.Policy; -import org.pgpainless.signature.subpackets.BaseSignatureSubpackets; -import org.pgpainless.signature.subpackets.SignatureSubpackets; -import org.pgpainless.signature.subpackets.SignatureSubpacketsHelper; - -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 - * @param hashAlgorithm hash algorithm used to generate the signature - * @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 - * @param hashAlgorithm hash algorithm used to generate the signature - * @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(); - } - - /** - * 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 - */ - public SigningOptions addSignature(SecretKeyRingProtector signingKeyProtector, - PGPSecretKeyRing signingKey) - throws PGPException { - return addInlineSignature(signingKeyProtector, signingKey, DocumentSignatureType.BINARY_DOCUMENT); - } - - /** - * 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 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 - */ - public SigningOptions addInlineSignatures(SecretKeyRingProtector secrectKeyDecryptor, - Iterable signingKeys, - DocumentSignatureType signatureType) - throws KeyException, 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) - * @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 - */ - public SigningOptions addInlineSignature(SecretKeyRingProtector secretKeyDecryptor, - PGPSecretKeyRing secretKey, - DocumentSignatureType signatureType) - throws KeyException, 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 KeyException if something is wrong with the key - * @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 KeyException, PGPException { - return addInlineSignature(secretKeyDecryptor, secretKey, userId, signatureType, null); - } - - /** - * 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) - * @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 - */ - public SigningOptions addInlineSignature(SecretKeyRingProtector secretKeyDecryptor, - PGPSecretKeyRing secretKey, - String userId, - DocumentSignatureType signatureType, - @Nullable BaseSignatureSubpackets.Callback subpacketsCallback) - throws KeyException, PGPException { - KeyRingInfo keyRingInfo = new KeyRingInfo(secretKey, new Date()); - if (userId != null && !keyRingInfo.isUserIdValid(userId)) { - throw new KeyException.UnboundUserIdException( - OpenPgpFingerprint.of(secretKey), - userId, - keyRingInfo.getLatestUserIdCertification(userId), - keyRingInfo.getUserIdRevocation(userId) - ); - } - - List signingPubKeys = keyRingInfo.getSigningSubkeys(); - if (signingPubKeys.isEmpty()) { - throw new KeyException.UnacceptableSigningKeyException(OpenPgpFingerprint.of(secretKey)); - } - - for (PGPPublicKey signingPubKey : signingPubKeys) { - PGPSecretKey signingSecKey = secretKey.getSecretKey(signingPubKey.getKeyID()); - if (signingSecKey == null) { - throw new KeyException.MissingSecretKeyException(OpenPgpFingerprint.of(secretKey), signingPubKey.getKeyID()); - } - PGPPrivateKey signingSubkey = UnlockSecretKey.unlockSecretKey(signingSecKey, secretKeyDecryptor); - Set hashAlgorithms = userId != null ? keyRingInfo.getPreferredHashAlgorithms(userId) - : keyRingInfo.getPreferredHashAlgorithms(signingPubKey.getKeyID()); - HashAlgorithm hashAlgorithm = negotiateHashAlgorithm(hashAlgorithms, PGPainless.getPolicy()); - addSigningMethod(secretKey, signingSubkey, subpacketsCallback, 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 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 - */ - public SigningOptions addDetachedSignatures(SecretKeyRingProtector secretKeyDecryptor, - Iterable signingKeys, - DocumentSignatureType signatureType) - throws PGPException { - for (PGPSecretKeyRing signingKey : signingKeys) { - addDetachedSignature(secretKeyDecryptor, signingKey, signatureType); - } - return this; - } - - /** - * Create a detached signature. - * The signature will be of type {@link DocumentSignatureType#BINARY_DOCUMENT}. - * - * @param secretKeyDecryptor decryptor to unlock the secret signing key - * @param signingKey signing key - * @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 - */ - public SigningOptions addDetachedSignature(SecretKeyRingProtector secretKeyDecryptor, - PGPSecretKeyRing signingKey) - throws PGPException { - return addDetachedSignature(secretKeyDecryptor, signingKey, DocumentSignatureType.BINARY_DOCUMENT); - } - - /** - * 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 secretKeyDecryptor decryptor to unlock the secret signing key - * @param secretKey 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 - */ - 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 (e.g. 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) - * @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 - */ - public SigningOptions addDetachedSignature(SecretKeyRingProtector secretKeyDecryptor, - PGPSecretKeyRing secretKey, - String userId, - DocumentSignatureType signatureType) - throws PGPException { - return addDetachedSignature(secretKeyDecryptor, secretKey, userId, signatureType, null); - } - - /** - * 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 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) - * @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 - */ - public SigningOptions addDetachedSignature(SecretKeyRingProtector secretKeyDecryptor, - PGPSecretKeyRing secretKey, - String userId, - DocumentSignatureType signatureType, - @Nullable BaseSignatureSubpackets.Callback subpacketCallback) - throws PGPException { - KeyRingInfo keyRingInfo = new KeyRingInfo(secretKey, new Date()); - if (userId != null && !keyRingInfo.isUserIdValid(userId)) { - throw new KeyException.UnboundUserIdException( - OpenPgpFingerprint.of(secretKey), - userId, - keyRingInfo.getLatestUserIdCertification(userId), - keyRingInfo.getUserIdRevocation(userId) - ); - } - - List signingPubKeys = keyRingInfo.getSigningSubkeys(); - if (signingPubKeys.isEmpty()) { - throw new KeyException.UnacceptableSigningKeyException(OpenPgpFingerprint.of(secretKey)); - } - - for (PGPPublicKey signingPubKey : signingPubKeys) { - PGPSecretKey signingSecKey = secretKey.getSecretKey(signingPubKey.getKeyID()); - if (signingSecKey == null) { - throw new KeyException.MissingSecretKeyException(OpenPgpFingerprint.of(secretKey), signingPubKey.getKeyID()); - } - PGPPrivateKey signingSubkey = UnlockSecretKey.unlockSecretKey(signingSecKey, secretKeyDecryptor); - Set hashAlgorithms = userId != null ? keyRingInfo.getPreferredHashAlgorithms(userId) - : keyRingInfo.getPreferredHashAlgorithms(signingPubKey.getKeyID()); - HashAlgorithm hashAlgorithm = negotiateHashAlgorithm(hashAlgorithms, PGPainless.getPolicy()); - addSigningMethod(secretKey, signingSubkey, subpacketCallback, hashAlgorithm, signatureType, true); - } - - return this; - } - - private void addSigningMethod(PGPSecretKeyRing secretKey, - PGPPrivateKey signingSubkey, - @Nullable BaseSignatureSubpackets.Callback subpacketCallback, - HashAlgorithm hashAlgorithm, - DocumentSignatureType signatureType, - boolean detached) - throws PGPException { - SubkeyIdentifier signingKeyIdentifier = new SubkeyIdentifier(secretKey, signingSubkey.getKeyID()); - PGPSecretKey signingSecretKey = secretKey.getSecretKey(signingSubkey.getKeyID()); - PublicKeyAlgorithm publicKeyAlgorithm = PublicKeyAlgorithm.requireFromId(signingSecretKey.getPublicKey().getAlgorithm()); - int bitStrength = secretKey.getPublicKey().getBitStrength(); - if (!PGPainless.getPolicy().getPublicKeyAlgorithmPolicy().isAcceptable(publicKeyAlgorithm, bitStrength)) { - throw new KeyException.UnacceptableSigningKeyException( - new KeyException.PublicKeyAlgorithmPolicyException( - OpenPgpFingerprint.of(secretKey), signingSecretKey.getKeyID(), publicKeyAlgorithm, bitStrength)); - } - - PGPSignatureGenerator generator = createSignatureGenerator(signingSubkey, hashAlgorithm, signatureType); - - // Subpackets - SignatureSubpackets hashedSubpackets = SignatureSubpackets.createHashedSubpackets(signingSecretKey.getPublicKey()); - SignatureSubpackets unhashedSubpackets = SignatureSubpackets.createEmptySubpackets(); - if (subpacketCallback != null) { - subpacketCallback.modifyHashedSubpackets(hashedSubpackets); - subpacketCallback.modifyUnhashedSubpackets(unhashedSubpackets); - } - generator.setHashedSubpackets(SignatureSubpacketsHelper.toVector(hashedSubpackets)); - generator.setUnhashedSubpackets(SignatureSubpacketsHelper.toVector(unhashedSubpackets)); - - SigningMethod signingMethod = detached ? - SigningMethod.detachedSignature(generator, hashAlgorithm) : - SigningMethod.inlineSignature(generator, hashAlgorithm); - signingMethods.put(signingKeyIdentifier, signingMethod); - } - - /** - * Negotiate, which hash algorithm to use. - * - * This method gives the 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; - } - - return HashAlgorithmNegotiator.negotiateSignatureHashAlgorithm(policy) - .negotiateHashAlgorithm(preferences); - } - - 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; - } - - /** - * Return a map of key-ids and signing methods. - * For internal use. - * - * @return signing methods - */ - 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/KeyException.java b/pgpainless-core/src/main/java/org/pgpainless/exception/KeyException.java index 66c5d2cf..65d27390 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/exception/KeyException.java +++ b/pgpainless-core/src/main/java/org/pgpainless/exception/KeyException.java @@ -67,6 +67,13 @@ public abstract class KeyException extends RuntimeException { } } + 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) { 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 c8e86311..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/implementation/BcImplementationFactory.java +++ /dev/null @@ -1,172 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.implementation; - -import java.io.InputStream; -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.PGPObjectFactory; -import org.bouncycastle.openpgp.PGPPrivateKey; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.PGPSessionKey; -import org.bouncycastle.openpgp.bc.BcPGPObjectFactory; -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.SessionKeyDataDecryptorFactory; -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; - -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.requireFromId(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 SessionKeyDataDecryptorFactory getSessionKeyDataDecryptorFactory(PGPSessionKey sessionKey) { - return new BcSessionKeyDataDecryptorFactory(sessionKey); - } - - @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()); - } - - @Override - public PGPObjectFactory getPGPObjectFactory(byte[] bytes) { - return new BcPGPObjectFactory(bytes); - } - - @Override - public PGPObjectFactory getPGPObjectFactory(InputStream inputStream) { - return new BcPGPObjectFactory(inputStream); - } - - 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 06065838..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/implementation/ImplementationFactory.java +++ /dev/null @@ -1,129 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.implementation; - -import java.io.InputStream; -import java.security.KeyPair; -import java.util.Date; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPKeyPair; -import org.bouncycastle.openpgp.PGPObjectFactory; -import org.bouncycastle.openpgp.PGPPrivateKey; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.PGPSessionKey; -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.SessionKeyDataDecryptorFactory; -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; - -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 getV4FingerprintCalculator() throws PGPException { - return getPGPDigestCalculator(HashAlgorithm.SHA1); - } - - 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 SessionKeyDataDecryptorFactory getSessionKeyDataDecryptorFactory(SessionKey sessionKey) { - PGPSessionKey pgpSessionKey = new PGPSessionKey( - sessionKey.getAlgorithm().getAlgorithmId(), - sessionKey.getKey() - ); - return getSessionKeyDataDecryptorFactory(pgpSessionKey); - } - - public abstract SessionKeyDataDecryptorFactory getSessionKeyDataDecryptorFactory(PGPSessionKey sessionKey); - - 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; - - public abstract PGPObjectFactory getPGPObjectFactory(InputStream inputStream); - - public abstract PGPObjectFactory getPGPObjectFactory(byte[] bytes); - - @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 31a7f128..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/implementation/JceImplementationFactory.java +++ /dev/null @@ -1,149 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.implementation; - -import java.io.InputStream; -import java.security.KeyPair; -import java.util.Date; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPKeyPair; -import org.bouncycastle.openpgp.PGPObjectFactory; -import org.bouncycastle.openpgp.PGPPrivateKey; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.PGPSessionKey; -import org.bouncycastle.openpgp.jcajce.JcaPGPObjectFactory; -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.SessionKeyDataDecryptorFactory; -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; - -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); - } - - @Override - public SessionKeyDataDecryptorFactory getSessionKeyDataDecryptorFactory(PGPSessionKey sessionKey) { - return new JceSessionKeyDataDecryptorFactoryBuilder() - .build(sessionKey); - } - - 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()); - } - - @Override - public PGPObjectFactory getPGPObjectFactory(InputStream inputStream) { - return new JcaPGPObjectFactory(inputStream); - } - - @Override - public PGPObjectFactory getPGPObjectFactory(byte[] bytes) { - return new JcaPGPObjectFactory(bytes); - } -} 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/OpenPgpFingerprint.java b/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpFingerprint.java deleted file mode 100644 index 86ac8265..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpFingerprint.java +++ /dev/null @@ -1,166 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key; - -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.PGPSecretKeyRing; -import org.bouncycastle.util.encoders.Hex; - -/** - * Abstract super class of different version OpenPGP fingerprints. - * - */ -public abstract class OpenPgpFingerprint implements CharSequence, Comparable { - @SuppressWarnings("CharsetObjectCanBeUsed") - protected static final Charset utf8 = Charset.forName("UTF-8"); - protected final String fingerprint; - - /** - * Return the fingerprint of the given key. - * This method automatically matches key versions to fingerprint implementations. - * - * @param key key - * @return fingerprint - */ - public static OpenPgpFingerprint of(PGPPublicKey key) { - if (key.getVersion() == 4) { - return new OpenPgpV4Fingerprint(key); - } - if (key.getVersion() == 5) { - return new OpenPgpV5Fingerprint(key); - } - throw new IllegalArgumentException("OpenPGP keys of version " + key.getVersion() + " 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 - */ - public static OpenPgpFingerprint of(PGPKeyRing ring) { - return of(ring.getPublicKey()); - } - - /** - * Try to parse an {@link OpenPgpFingerprint} from the given fingerprint string. - * - * @param fingerprint fingerprint - * @return parsed fingerprint - */ - public static OpenPgpFingerprint parse(String fingerprint) { - String fp = fingerprint.replace(" ", "").trim().toUpperCase(); - if (fp.matches("^[0-9A-F]{40}$")) { - return new OpenPgpV4Fingerprint(fp); - } - if (fp.matches("^[0-9A-F]{64}$")) { - return new OpenPgpV5Fingerprint(fp); - } - throw new IllegalArgumentException("Fingerprint does not appear to match any known fingerprint patterns."); - } - - /** - * Parse a binary OpenPGP fingerprint into an {@link OpenPgpFingerprint} object. - * - * @param binaryFingerprint binary representation of the fingerprint - * @return parsed fingerprint - */ - public static OpenPgpFingerprint parseFromBinary(byte[] binaryFingerprint) { - String hex = Hex.toHexString(binaryFingerprint).toUpperCase(); - return parse(hex); - } - - public OpenPgpFingerprint(String fingerprint) { - String fp = fingerprint.replace(" ", "").trim().toUpperCase(); - if (!isValid(fp)) { - throw new IllegalArgumentException( - String.format("Fingerprint '%s' does not appear to be a valid OpenPGP V%d fingerprint.", fingerprint, getVersion()) - ); - } - this.fingerprint = fp; - } - - public OpenPgpFingerprint(@Nonnull byte[] bytes) { - this(new String(bytes, utf8)); - } - - public OpenPgpFingerprint(PGPPublicKey key) { - this(Hex.encode(key.getFingerprint())); - if (key.getVersion() != getVersion()) { - throw new IllegalArgumentException(String.format("Key is not a v%d OpenPgp key.", getVersion())); - } - } - - public OpenPgpFingerprint(@Nonnull PGPPublicKeyRing ring) { - this(ring.getPublicKey()); - } - - public OpenPgpFingerprint(@Nonnull PGPSecretKeyRing ring) { - this(ring.getPublicKey()); - } - - public OpenPgpFingerprint(@Nonnull PGPKeyRing ring) { - this(ring.getPublicKey()); - } - - /** - * Return the version of the fingerprint. - * - * @return version - */ - public abstract int getVersion(); - - /** - * Check, whether the fingerprint consists of 40 valid hexadecimal characters. - * @param fp fingerprint to check. - * @return true if fingerprint is valid. - */ - protected abstract boolean isValid(@Nonnull String fp); - - /** - * Return the key id of the OpenPGP public key this {@link 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. - * - * @see - * RFC-4880 §12.2: Key IDs and Fingerprints - * @return key id - */ - public abstract long getKeyId(); - - @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; - } - - /** - * Return a pretty printed representation of the fingerprint. - * - * @return pretty printed fingerprint - */ - public abstract String prettyPrint(); -} 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 13a79201..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpV4Fingerprint.java +++ /dev/null @@ -1,149 +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 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 a hex encoded, uppercase OpenPGP v4 fingerprint. - */ -public class OpenPgpV4Fingerprint extends OpenPgpFingerprint { - - public static final String SCHEME = "openpgp4fpr"; - - /** - * Create an {@link OpenPgpV4Fingerprint}. - * - * @param fingerprint uppercase hexadecimal fingerprint of length 40 - */ - public OpenPgpV4Fingerprint(@Nonnull String fingerprint) { - super(fingerprint); - } - - public OpenPgpV4Fingerprint(@Nonnull byte[] bytes) { - super(Hex.encode(bytes)); - } - - public OpenPgpV4Fingerprint(@Nonnull PGPPublicKey key) { - super(key); - } - - public OpenPgpV4Fingerprint(@Nonnull PGPSecretKey key) { - this(key.getPublicKey()); - } - - public OpenPgpV4Fingerprint(@Nonnull PGPPublicKeyRing ring) { - super(ring); - } - - public OpenPgpV4Fingerprint(@Nonnull PGPSecretKeyRing ring) { - super(ring); - } - - public OpenPgpV4Fingerprint(@Nonnull PGPKeyRing ring) { - super(ring); - } - - @Override - public int getVersion() { - return 4; - } - - @Override - protected boolean isValid(@Nonnull String fp) { - return fp.matches("^[0-9A-F]{40}$"); - } - - @Override - public long getKeyId() { - byte[] bytes = Hex.decode(toString().getBytes(utf8)); - ByteBuffer 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 - ((Buffer) buf).position(12); // 20 - 8 bytes = offset 12 - - return buf.getLong(); - } - - @Override - 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(); - } - - @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 toString().hashCode(); - } - - /** - * 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(OpenPgpV4Fingerprint.SCHEME, toString(), null); - } catch (URISyntaxException e) { - throw new AssertionError(e); - } - } - - /** - * Convert an 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 OpenPgpFingerprint openPgpFingerprint) { - return toString().compareTo(openPgpFingerprint.toString()); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpV5Fingerprint.java b/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpV5Fingerprint.java deleted file mode 100644 index fafbc34c..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpV5Fingerprint.java +++ /dev/null @@ -1,117 +0,0 @@ -// 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 javax.annotation.Nonnull; -import java.nio.Buffer; -import java.nio.ByteBuffer; - -/** - * This class represents a hex encoded, upper case OpenPGP v5 fingerprint. - */ -public class OpenPgpV5Fingerprint extends OpenPgpFingerprint { - - /** - * Create an {@link OpenPgpV5Fingerprint}. - * - * @param fingerprint uppercase hexadecimal fingerprint of length 64 - */ - public OpenPgpV5Fingerprint(@Nonnull String fingerprint) { - super(fingerprint); - } - - public OpenPgpV5Fingerprint(@Nonnull byte[] bytes) { - super(Hex.encode(bytes)); - } - - public OpenPgpV5Fingerprint(@Nonnull PGPPublicKey key) { - super(key); - } - - public OpenPgpV5Fingerprint(@Nonnull PGPSecretKey key) { - this(key.getPublicKey()); - } - - public OpenPgpV5Fingerprint(@Nonnull PGPPublicKeyRing ring) { - super(ring); - } - - public OpenPgpV5Fingerprint(@Nonnull PGPSecretKeyRing ring) { - super(ring); - } - - public OpenPgpV5Fingerprint(@Nonnull PGPKeyRing ring) { - super(ring); - } - - @Override - public int getVersion() { - return 5; - } - - @Override - protected boolean isValid(@Nonnull String fp) { - return fp.matches("^[0-9A-F]{64}$"); - } - - @Override - public long getKeyId() { - byte[] bytes = Hex.decode(toString().getBytes(utf8)); - ByteBuffer 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 - ((Buffer) buf).position(0); - - return buf.getLong(); - } - - @Override - public String prettyPrint() { - String fp = toString(); - StringBuilder pretty = new StringBuilder(); - - for (int i = 0; i < 4; i++) { - pretty.append(fp, i * 8, (i + 1) * 8).append(' '); - } - pretty.append(' '); - for (int i = 4; i < 7; i++) { - pretty.append(fp, i * 8, (i + 1) * 8).append(' '); - } - pretty.append(fp, 56, 64); - return pretty.toString(); - } - - @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 toString().hashCode(); - } - - @Override - public int compareTo(OpenPgpFingerprint openPgpFingerprint) { - return toString().compareTo(openPgpFingerprint.toString()); - } -} 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 8bfd7f7c..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 OpenPgpFingerprint primaryKeyFingerprint; - private final OpenPgpFingerprint 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 OpenPgpFingerprint} 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 = OpenPgpFingerprint.of(keyRing); - this.subkeyFingerprint = OpenPgpFingerprint.of(subkey); - } - - public SubkeyIdentifier(@Nonnull PGPKeyRing keyRing, @Nonnull OpenPgpFingerprint subkeyFingerprint) { - this(OpenPgpFingerprint.of(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 OpenPgpFingerprint 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 OpenPgpFingerprint primaryKeyFingerprint, @Nonnull OpenPgpFingerprint subkeyFingerprint) { - this.primaryKeyFingerprint = primaryKeyFingerprint; - this.subkeyFingerprint = subkeyFingerprint; - } - - public @Nonnull OpenPgpFingerprint getFingerprint() { - return getSubkeyFingerprint(); - } - - public long getKeyId() { - return getSubkeyId(); - } - - /** - * Return the {@link OpenPgpFingerprint} 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 OpenPgpFingerprint 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 OpenPgpFingerprint} of the identified subkey. - * - * @return subkey fingerprint - */ - public @Nonnull OpenPgpFingerprint 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/certification/CertifyCertificate.java b/pgpainless-core/src/main/java/org/pgpainless/key/certification/CertifyCertificate.java deleted file mode 100644 index 05e64879..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/certification/CertifyCertificate.java +++ /dev/null @@ -1,293 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.certification; - -import org.bouncycastle.openpgp.PGPException; -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.CertificationType; -import org.pgpainless.algorithm.KeyFlag; -import org.pgpainless.algorithm.Trustworthiness; -import org.pgpainless.exception.KeyException; -import org.pgpainless.key.OpenPgpFingerprint; -import org.pgpainless.key.info.KeyRingInfo; -import org.pgpainless.key.protection.SecretKeyRingProtector; -import org.pgpainless.key.util.KeyRingUtils; -import org.pgpainless.signature.builder.ThirdPartyDirectKeySignatureBuilder; -import org.pgpainless.signature.builder.ThirdPartyCertificationSignatureBuilder; -import org.pgpainless.signature.subpackets.CertificationSubpackets; -import org.pgpainless.util.DateUtil; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.util.Date; - -/** - * 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. - */ -public class CertifyCertificate { - - /** - * Create a certification over a User-Id. - * By default, this method will use {@link CertificationType#GENERIC} to create the signature. - * If you need to create another type of certification, use - * {@link #userIdOnCertificate(String, PGPPublicKeyRing, CertificationType)} instead. - * - * @param userId user-id to certify - * @param certificate certificate - * @return API - */ - public CertificationOnUserId userIdOnCertificate(@Nonnull String userId, - @Nonnull PGPPublicKeyRing certificate) { - return userIdOnCertificate(userId, certificate, CertificationType.GENERIC); - } - - /** - * Create a certification of the given {@link CertificationType} over a User-Id. - * - * @param userid user-id to certify - * @param certificate certificate - * @param certificationType type of signature - * @return API - */ - public CertificationOnUserId userIdOnCertificate(@Nonnull String userid, - @Nonnull PGPPublicKeyRing certificate, - @Nonnull CertificationType certificationType) { - return new 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 {@link #certificate(PGPPublicKeyRing, Trustworthiness)}). - * - * @param certificate certificate - * @return API - */ - public DelegationOnCertificate certificate(@Nonnull PGPPublicKeyRing certificate) { - return certificate(certificate, null); - } - - /** - * Create a delegation (direct key signature) containing a {@link 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 - */ - public DelegationOnCertificate certificate(@Nonnull PGPPublicKeyRing certificate, - @Nullable Trustworthiness trustworthiness) { - return new DelegationOnCertificate(certificate, trustworthiness); - } - - public static class CertificationOnUserId { - - private final PGPPublicKeyRing certificate; - private final String userId; - private final CertificationType certificationType; - - CertificationOnUserId(@Nonnull String userId, - @Nonnull PGPPublicKeyRing certificate, - @Nonnull CertificationType certificationType) { - this.userId = userId; - this.certificate = certificate; - this.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 - */ - public CertificationOnUserIdWithSubpackets withKey(@Nonnull PGPSecretKeyRing certificationKey, - @Nonnull SecretKeyRingProtector protector) - throws PGPException { - PGPSecretKey secretKey = getCertifyingSecretKey(certificationKey); - - ThirdPartyCertificationSignatureBuilder sigBuilder = new ThirdPartyCertificationSignatureBuilder( - certificationType.asSignatureType(), secretKey, protector); - - return new CertificationOnUserIdWithSubpackets(certificate, userId, sigBuilder); - } - } - - public static class CertificationOnUserIdWithSubpackets { - - private final PGPPublicKeyRing certificate; - private final String userId; - private final ThirdPartyCertificationSignatureBuilder sigBuilder; - - CertificationOnUserIdWithSubpackets(@Nonnull PGPPublicKeyRing certificate, - @Nonnull String userId, - @Nonnull ThirdPartyCertificationSignatureBuilder sigBuilder) { - this.certificate = certificate; - this.userId = userId; - this.sigBuilder = sigBuilder; - } - - /** - * 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 - */ - public CertificationResult buildWithSubpackets(@Nonnull CertificationSubpackets.Callback subpacketCallback) - throws PGPException { - sigBuilder.applyCallback(subpacketCallback); - return build(); - } - - /** - * Build the certification signature. - * - * @return result - * @throws PGPException in case of an OpenPGP related error - */ - public CertificationResult build() throws PGPException { - PGPSignature signature = sigBuilder.build(certificate, userId); - PGPPublicKeyRing certifiedCertificate = KeyRingUtils.injectCertification(certificate, userId, signature); - return new CertificationResult(certifiedCertificate, signature); - } - } - - public static class DelegationOnCertificate { - - private final PGPPublicKeyRing certificate; - private final Trustworthiness trustworthiness; - - DelegationOnCertificate(@Nonnull PGPPublicKeyRing certificate, - @Nullable Trustworthiness trustworthiness) { - this.certificate = certificate; - this.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 - */ - public DelegationOnCertificateWithSubpackets withKey(@Nonnull PGPSecretKeyRing certificationKey, - @Nonnull SecretKeyRingProtector protector) - throws PGPException { - PGPSecretKey secretKey = getCertifyingSecretKey(certificationKey); - - ThirdPartyDirectKeySignatureBuilder sigBuilder = new ThirdPartyDirectKeySignatureBuilder(secretKey, protector); - if (trustworthiness != null) { - sigBuilder.getHashedSubpackets().setTrust(true, trustworthiness.getDepth(), trustworthiness.getAmount()); - } - return new DelegationOnCertificateWithSubpackets(certificate, sigBuilder); - } - } - - public static class DelegationOnCertificateWithSubpackets { - - private final PGPPublicKeyRing certificate; - private final ThirdPartyDirectKeySignatureBuilder sigBuilder; - - DelegationOnCertificateWithSubpackets(@Nonnull PGPPublicKeyRing certificate, - @Nonnull ThirdPartyDirectKeySignatureBuilder sigBuilder) { - this.certificate = certificate; - this.sigBuilder = sigBuilder; - } - - /** - * 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 - */ - public CertificationResult buildWithSubpackets(@Nonnull CertificationSubpackets.Callback subpacketsCallback) - throws PGPException { - sigBuilder.applyCallback(subpacketsCallback); - return build(); - } - - /** - * Build the delegation signature. - * - * @return result - * @throws PGPException in case of an OpenPGP related error - */ - public CertificationResult build() throws PGPException { - PGPPublicKey delegatedKey = certificate.getPublicKey(); - PGPSignature delegation = sigBuilder.build(delegatedKey); - PGPPublicKeyRing delegatedCertificate = KeyRingUtils.injectCertification(certificate, delegatedKey, delegation); - return new CertificationResult(delegatedCertificate, delegation); - } - } - - public static class CertificationResult { - - private final PGPPublicKeyRing certificate; - private final PGPSignature certification; - - CertificationResult(@Nonnull PGPPublicKeyRing certificate, @Nonnull PGPSignature certification) { - this.certificate = certificate; - this.certification = certification; - } - - /** - * Return the signature. - * - * @return signature - */ - @Nonnull - public PGPSignature getCertification() { - return certification; - } - - /** - * Return the certificate, which now contains the signature. - * - * @return certificate + signature - */ - @Nonnull - public PGPPublicKeyRing getCertifiedCertificate() { - return certificate; - } - } - - private static PGPSecretKey getCertifyingSecretKey(PGPSecretKeyRing certificationKey) { - Date now = DateUtil.now(); - KeyRingInfo info = PGPainless.inspectKeyRing(certificationKey, now); - - // We only support certification-capable primary keys - OpenPgpFingerprint fingerprint = info.getFingerprint(); - PGPPublicKey certificationPubKey = info.getPublicKey(fingerprint); - if (!info.isKeyValidlyBound(certificationPubKey.getKeyID())) { - throw new KeyException.RevokedKeyException(fingerprint); - } - - Date expirationDate = info.getExpirationDateForUse(KeyFlag.CERTIFY_OTHER); - if (expirationDate != null && expirationDate.before(now)) { - throw new KeyException.ExpiredKeyException(fingerprint, expirationDate); - } - - PGPSecretKey secretKey = certificationKey.getSecretKey(certificationPubKey.getKeyID()); - if (secretKey == null) { - throw new KeyException.MissingSecretKeyException(fingerprint, certificationPubKey.getKeyID()); - } - return secretKey; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/certification/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/key/certification/package-info.java deleted file mode 100644 index db2f4857..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/certification/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * API for key certifications. - */ -package org.pgpainless.key.certification; 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 6cf7102d..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 = ImplementationFactory.getInstance().getPGPObjectFactory(decoderStream); - 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 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 10970fc2..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingBuilder.java +++ /dev/null @@ -1,317 +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.LinkedHashMap; -import java.util.List; -import java.util.Map; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import org.bouncycastle.bcpg.sig.KeyFlags; -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.PublicKeyAlgorithm; -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.protection.UnlockSecretKey; -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; - -public class KeyRingBuilder implements KeyRingBuilderInterface { - - @SuppressWarnings("CharsetObjectCanBeUsed") - private final Charset UTF8 = Charset.forName("UTF-8"); - - private KeySpec primaryKeySpec; - private final List subkeySpecs = new ArrayList<>(); - private final Map userIds = new LinkedHashMap<>(); - private Passphrase passphrase = Passphrase.emptyPassphrase(); - private Date expirationDate = null; - - @Override - public KeyRingBuilder setPrimaryKey(@Nonnull KeySpec keySpec) { - verifyKeySpecCompliesToPolicy(keySpec, PGPainless.getPolicy()); - verifyMasterKeyCanCertify(keySpec); - this.primaryKeySpec = keySpec; - return this; - } - - @Override - public KeyRingBuilder addSubkey(@Nonnull KeySpec keySpec) { - verifyKeySpecCompliesToPolicy(keySpec, PGPainless.getPolicy()); - this.subkeySpecs.add(keySpec); - return this; - } - - @Override - public KeyRingBuilder addUserId(@Nonnull String userId) { - this.userIds.put(userId.trim(), null); - return this; - } - - public KeyRingBuilder addUserId( - @Nonnull String userId, - @Nullable SelfSignatureSubpackets.Callback subpacketsCallback) { - this.userIds.put(userId.trim(), subpacketsCallback); - 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 void verifyKeySpecCompliesToPolicy(KeySpec keySpec, Policy policy) { - PublicKeyAlgorithm publicKeyAlgorithm = keySpec.getKeyType().getAlgorithm(); - int bitStrength = keySpec.getKeyType().getBitStrength(); - - if (!policy.getPublicKeyAlgorithmPolicy().isAcceptable(publicKeyAlgorithm, bitStrength)) { - throw new IllegalArgumentException("Public key algorithm policy violation: " + - publicKeyAlgorithm + " with bit strength " + bitStrength + " is not acceptable."); - } - } - - 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) { - KeyFlags keyFlags = keySpec.getSubpacketGenerator().getKeyFlagsSubpacket(); - return keyFlags != null && KeyFlag.hasKeyFlag(keyFlags.getFlags(), KeyFlag.CERTIFY_OTHER); - } - - private boolean keyIsCertificationCapable(KeySpec keySpec) { - return keySpec.getKeyType().canCertify(); - } - - @Override - public PGPSecretKeyRing build() throws NoSuchAlgorithmException, PGPException, - InvalidAlgorithmParameterException { - PGPDigestCalculator keyFingerprintCalculator = ImplementationFactory.getInstance().getV4FingerprintCalculator(); - PBESecretKeyEncryptor secretKeyEncryptor = buildSecretKeyEncryptor(keyFingerprintCalculator); - PBESecretKeyDecryptor secretKeyDecryptor = buildSecretKeyDecryptor(); - - passphrase.clear(); - - // Generate Primary Key - PGPKeyPair certKey = generateKeyPair(primaryKeySpec); - PGPContentSignerBuilder signer = buildContentSigner(certKey); - PGPSignatureGenerator signatureGenerator = new PGPSignatureGenerator(signer); - - SignatureSubpackets hashedSubPacketGenerator = primaryKeySpec.getSubpacketGenerator(); - hashedSubPacketGenerator.setIssuerFingerprintAndKeyId(certKey.getPublicKey()); - if (expirationDate != null) { - hashedSubPacketGenerator.setKeyExpirationTime(certKey.getPublicKey(), expirationDate); - } - if (!userIds.isEmpty()) { - hashedSubPacketGenerator.setPrimaryUserId(); - } - - PGPSignatureSubpacketGenerator generator = new PGPSignatureSubpacketGenerator(); - SignatureSubpacketsHelper.applyTo(hashedSubPacketGenerator, generator); - PGPSignatureSubpacketVector hashedSubPackets = generator.generate(); - PGPKeyRingGenerator ringGenerator; - if (userIds.isEmpty()) { - ringGenerator = new PGPKeyRingGenerator( - certKey, - keyFingerprintCalculator, - hashedSubPackets, - null, - signer, - secretKeyEncryptor); - } else { - String primaryUserId = userIds.entrySet().iterator().next().getKey(); - ringGenerator = new PGPKeyRingGenerator( - SignatureType.POSITIVE_CERTIFICATION.getCode(), certKey, - primaryUserId, keyFingerprintCalculator, - hashedSubPackets, null, signer, secretKeyEncryptor); - } - - 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> userIdIterator = - this.userIds.entrySet().iterator(); - if (userIdIterator.hasNext()) { - userIdIterator.next(); // Skip primary user id - } - while (userIdIterator.hasNext()) { - Map.Entry additionalUserId = userIdIterator.next(); - String userIdString = additionalUserId.getKey(); - SelfSignatureSubpackets.Callback callback = additionalUserId.getValue(); - SelfSignatureSubpackets subpackets = null; - if (callback == null) { - subpackets = hashedSubPacketGenerator; - subpackets.setPrimaryUserId(null); - // additional user-ids are not primary - } else { - subpackets = SignatureSubpackets.createHashedSubpackets(primaryPubKey); - callback.modifyHashedSubpackets(subpackets); - } - signatureGenerator.init(SignatureType.POSITIVE_CERTIFICATION.getCode(), privateKey); - signatureGenerator.setHashedSubpackets( - SignatureSubpacketsHelper.toVector((SignatureSubpackets) subpackets)); - PGPSignature additionalUserIdSignature = - signatureGenerator.generateCertification(userIdString, primaryPubKey); - primaryPubKey = PGPPublicKey.addCertification(primaryPubKey, - userIdString, additionalUserIdSignature); - } - - // "reassemble" secret key ring with modified primary key - PGPSecretKey primarySecKey = new PGPSecretKey( - privateKey, primaryPubKey, keyFingerprintCalculator, true, secretKeyEncryptor); - List secretKeyList = new ArrayList<>(); - secretKeyList.add(primarySecKey); - while (secretKeys.hasNext()) { - secretKeyList.add(secretKeys.next()); - } - secretKeyRing = new PGPSecretKeyRing(secretKeyList); - return secretKeyRing; - } - - 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(PGPDigestCalculator keyFingerprintCalculator) { - SymmetricKeyAlgorithm keyEncryptionAlgorithm = PGPainless.getPolicy() - .getSymmetricKeyEncryptionAlgorithmPolicy() - .getDefaultSymmetricKeyAlgorithm(); - if (!passphrase.isValid()) { - throw new IllegalStateException("Passphrase was cleared."); - } - return passphrase.isEmpty() ? null : // unencrypted key pair, otherwise AES-256 encrypted - ImplementationFactory.getInstance().getPBESecretKeyEncryptor( - keyEncryptionAlgorithm, keyFingerprintCalculator, passphrase); - } - - private PBESecretKeyDecryptor buildSecretKeyDecryptor() throws PGPException { - if (!passphrase.isValid()) { - throw new IllegalStateException("Passphrase was cleared."); - } - return passphrase.isEmpty() ? null : - ImplementationFactory.getInstance().getPBESecretKeyDecryptor(passphrase); - } - - 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(); - - Date keyCreationDate = spec.getKeyCreationDate() != null ? spec.getKeyCreationDate() : new Date(); - - // Form PGP key pair - PGPKeyPair pgpKeyPair = ImplementationFactory.getInstance() - .getPGPKeyPair(type.getAlgorithm(), keyPair, keyCreationDate); - 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/KeyRingTemplates.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingTemplates.java deleted file mode 100644 index 42eb7efa..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingTemplates.java +++ /dev/null @@ -1,258 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.generation; - -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.PGPSecretKeyRing; -import org.pgpainless.PGPainless; -import org.pgpainless.algorithm.KeyFlag; -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.util.UserId; -import org.pgpainless.util.Passphrase; - -public final class KeyRingTemplates { - - public KeyRingTemplates() { - - } - - /** - * 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. - * - * @throws InvalidAlgorithmParameterException in case of invalid key generation parameters - * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider - * @throws PGPException in case of an OpenPGP related error - */ - public PGPSecretKeyRing simpleRsaKeyRing(@Nullable UserId userId, @Nonnull RsaLength length) - throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { - return simpleRsaKeyRing(userId == null ? null : 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. - * - * @throws InvalidAlgorithmParameterException in case of invalid key generation parameters - * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider - * @throws PGPException in case of an OpenPGP related error - */ - public PGPSecretKeyRing simpleRsaKeyRing(@Nullable String userId, @Nonnull RsaLength length) - throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { - return simpleRsaKeyRing(userId, length, Passphrase.emptyPassphrase()); - } - - /** - * 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. - * - * @throws InvalidAlgorithmParameterException in case of invalid key generation parameters - * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider - * @throws PGPException in case of an OpenPGP related error - */ - public PGPSecretKeyRing simpleRsaKeyRing(@Nullable UserId userId, @Nonnull RsaLength length, @Nullable String password) - throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { - return simpleRsaKeyRing(userId == null ? null : 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. - * - * @throws InvalidAlgorithmParameterException in case of invalid key generation parameters - * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider - * @throws PGPException in case of an OpenPGP related error - */ - public PGPSecretKeyRing simpleRsaKeyRing(@Nullable String userId, @Nonnull RsaLength length, @Nullable String password) - throws PGPException, NoSuchAlgorithmException, InvalidAlgorithmParameterException { - Passphrase passphrase = Passphrase.emptyPassphrase(); - if (!isNullOrEmpty(password)) { - passphrase = Passphrase.fromPassword(password); - } - return simpleRsaKeyRing(userId, length, passphrase); - } - - public PGPSecretKeyRing simpleRsaKeyRing(@Nullable String userId, @Nonnull RsaLength length, @Nonnull Passphrase passphrase) - throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - KeyRingBuilder builder = PGPainless.buildKeyRing() - .setPrimaryKey(KeySpec.getBuilder(KeyType.RSA(length), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA, KeyFlag.ENCRYPT_COMMS)) - .setPassphrase(passphrase); - if (userId != null) { - builder.addUserId(userId); - } - 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. - * - * @throws InvalidAlgorithmParameterException in case of invalid key generation parameters - * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider - * @throws PGPException in case of an OpenPGP related error - */ - public PGPSecretKeyRing simpleEcKeyRing(@Nullable UserId userId) - throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { - return simpleEcKeyRing(userId == null ? null : 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. - * - * @throws InvalidAlgorithmParameterException in case of invalid key generation parameters - * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider - * @throws PGPException in case of an OpenPGP related error - */ - public PGPSecretKeyRing simpleEcKeyRing(@Nullable String userId) - throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { - return simpleEcKeyRing(userId, Passphrase.emptyPassphrase()); - } - - /** - * 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. - * - * @throws InvalidAlgorithmParameterException in case of invalid key generation parameters - * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider - * @throws PGPException in case of an OpenPGP related error - */ - public PGPSecretKeyRing simpleEcKeyRing(@Nullable UserId userId, String password) - throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { - return simpleEcKeyRing(userId == null ? null : 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. - * - * @throws InvalidAlgorithmParameterException in case of invalid key generation parameters - * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider - * @throws PGPException in case of an OpenPGP related error - */ - public PGPSecretKeyRing simpleEcKeyRing(@Nullable String userId, String password) - throws PGPException, NoSuchAlgorithmException, InvalidAlgorithmParameterException { - Passphrase passphrase = Passphrase.emptyPassphrase(); - if (!isNullOrEmpty(password)) { - passphrase = Passphrase.fromPassword(password); - } - return simpleEcKeyRing(userId, passphrase); - } - - public PGPSecretKeyRing simpleEcKeyRing(@Nullable String userId, @Nonnull Passphrase passphrase) - throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - KeyRingBuilder builder = PGPainless.buildKeyRing() - .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)) - .setPassphrase(passphrase); - if (userId != null) { - builder.addUserId(userId); - } - 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 - * @return key ring - * - * @throws InvalidAlgorithmParameterException in case of invalid key generation parameters - * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider - * @throws PGPException in case of an OpenPGP related error - */ - public PGPSecretKeyRing modernKeyRing(@Nullable String userId) throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - return modernKeyRing(userId, Passphrase.emptyPassphrase()); - } - - /** - * 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 - * - * @throws InvalidAlgorithmParameterException in case of invalid key generation parameters - * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider - * @throws PGPException in case of an OpenPGP related error - */ - public PGPSecretKeyRing modernKeyRing(@Nullable String userId, @Nullable String password) - throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { - Passphrase passphrase = (password != null ? Passphrase.fromPassword(password) : Passphrase.emptyPassphrase()); - return modernKeyRing(userId, passphrase); - } - - public PGPSecretKeyRing modernKeyRing(@Nullable String userId, @Nonnull Passphrase passphrase) - throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - KeyRingBuilder builder = PGPainless.buildKeyRing() - .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)) - .setPassphrase(passphrase); - if (userId != null) { - builder.addUserId(userId); - } - return builder.build(); - } - - private static boolean isNullOrEmpty(String password) { - return password == null || password.trim().isEmpty(); - } - -} 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 63645edd..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeySpec.java +++ /dev/null @@ -1,62 +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.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; - -import java.util.Date; - -public class KeySpec { - - private final KeyType keyType; - private final SignatureSubpackets subpacketGenerator; - private final boolean inheritedSubPackets; - private final Date keyCreationDate; - - KeySpec(@Nonnull KeyType type, - @Nonnull SignatureSubpackets subpacketGenerator, - boolean inheritedSubPackets, - @Nullable Date keyCreationDate) { - this.keyType = type; - this.subpacketGenerator = subpacketGenerator; - this.inheritedSubPackets = inheritedSubPackets; - this.keyCreationDate = keyCreationDate; - } - - @Nonnull - public KeyType getKeyType() { - return keyType; - } - - @Nonnull - public PGPSignatureSubpacketVector getSubpackets() { - return SignatureSubpacketsHelper.toVector(subpacketGenerator); - } - - @Nonnull - public SignatureSubpackets getSubpacketGenerator() { - return subpacketGenerator; - } - - boolean isInheritedSubPackets() { - return inheritedSubPackets; - } - - @Nullable - public Date getKeyCreationDate() { - return keyCreationDate; - } - - 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 559dd3ce..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeySpecBuilder.java +++ /dev/null @@ -1,92 +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.Date; -import java.util.LinkedHashSet; -import java.util.Set; -import javax.annotation.Nonnull; - -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.signature.subpackets.SelfSignatureSubpackets; -import org.pgpainless.signature.subpackets.SignatureSubpackets; -import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil; -import org.pgpainless.util.CollectionUtils; - -public class KeySpecBuilder implements KeySpecBuilderInterface { - - private final KeyType type; - private final KeyFlag[] keyFlags; - private final SelfSignatureSubpackets hashedSubpackets = new SignatureSubpackets(); - private final AlgorithmSuite algorithmSuite = PGPainless.getPolicy().getKeyGenerationAlgorithmSuite(); - private Set preferredCompressionAlgorithms = algorithmSuite.getCompressionAlgorithms(); - private Set preferredHashAlgorithms = algorithmSuite.getHashAlgorithms(); - private Set preferredSymmetricAlgorithms = algorithmSuite.getSymmetricKeyAlgorithms(); - private Date keyCreationDate; - - 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); - SignatureSubpacketsUtil.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) { - for (SymmetricKeyAlgorithm algo : preferredSymmetricKeyAlgorithms) { - if (algo == SymmetricKeyAlgorithm.NULL) { - throw new IllegalArgumentException("NULL (unencrypted) is an invalid symmetric key algorithm preference."); - } - } - this.preferredSymmetricAlgorithms = new LinkedHashSet<>(Arrays.asList(preferredSymmetricKeyAlgorithms)); - return this; - } - - @Override - public KeySpecBuilder setKeyCreationDate(@Nonnull Date creationDate) { - this.keyCreationDate = creationDate; - return this; - } - - @Override - public KeySpec build() { - this.hashedSubpackets.setKeyFlags(keyFlags); - this.hashedSubpackets.setPreferredCompressionAlgorithms(preferredCompressionAlgorithms); - this.hashedSubpackets.setPreferredHashAlgorithms(preferredHashAlgorithms); - this.hashedSubpackets.setPreferredSymmetricKeyAlgorithms(preferredSymmetricAlgorithms); - this.hashedSubpackets.setFeatures(Feature.MODIFICATION_DETECTION); - - return new KeySpec(type, (SignatureSubpackets) hashedSubpackets, false, keyCreationDate); - } -} 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 4a99bc8d..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeySpecBuilderInterface.java +++ /dev/null @@ -1,26 +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; - -import java.util.Date; - -public interface KeySpecBuilderInterface { - - KeySpecBuilder overridePreferredCompressionAlgorithms(@Nonnull CompressionAlgorithm... compressionAlgorithms); - - KeySpecBuilder overridePreferredHashAlgorithms(@Nonnull HashAlgorithm... preferredHashAlgorithms); - - KeySpecBuilder overridePreferredSymmetricKeyAlgorithms(@Nonnull SymmetricKeyAlgorithm... preferredSymmetricKeyAlgorithms); - - KeySpecBuilder setKeyCreationDate(@Nonnull Date creationDate); - - 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 191a22f7..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/KeyType.java +++ /dev/null @@ -1,118 +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 the strength of the key in bits. - * @return strength of the key in bits - */ - int getBitStrength(); - - /** - * 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 can 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 2372896e..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/ecc/EllipticCurve.java +++ /dev/null @@ -1,42 +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", 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 - _SECP256K1("secp256k1", 256), - _BRAINPOOLP256R1("brainpoolP256r1", 256), - _BRAINPOOLP384R1("brainpoolP384r1", 384), - _BRAINPOOLP512R1("brainpoolP512r1", 512) - ; - - private final String name; - private final int bitStrength; - - EllipticCurve(@Nonnull String name, int bitStrength) { - this.name = name; - this.bitStrength = bitStrength; - } - - public String getName() { - return name; - } - - public int getBitStrength() { - return bitStrength; - } -} 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 bb7e3f3c..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/ecc/ecdh/ECDH.java +++ /dev/null @@ -1,46 +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 int getBitStrength() { - return curve.getBitStrength(); - } - - @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 87301655..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/ecc/ecdsa/ECDSA.java +++ /dev/null @@ -1,48 +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 int getBitStrength() { - return curve.getBitStrength(); - } - - @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 ae46b44f..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/eddsa/EdDSA.java +++ /dev/null @@ -1,48 +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 int getBitStrength() { - return curve.getBitStrength(); - } - - @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 4d5aed1c..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/eddsa/EdDSACurve.java +++ /dev/null @@ -1,28 +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", 256), - ; - - final String name; - final int bitStrength; - - EdDSACurve(@Nonnull String curveName, int bitStrength) { - this.name = curveName; - this.bitStrength = bitStrength; - } - - public String getName() { - return name; - } - - public int getBitStrength() { - return bitStrength; - } -} 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 4deb0559..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/elgamal/ElGamal.java +++ /dev/null @@ -1,49 +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 int getBitStrength() { - return length.getLength(); - } - - @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 3cf717b2..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/rsa/RSA.java +++ /dev/null @@ -1,48 +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 int getBitStrength() { - return length.getLength(); - } - - @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 4e589677..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/xdh/XDH.java +++ /dev/null @@ -1,45 +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 int getBitStrength() { - return spec.getBitStrength(); - } - - @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 ccbd2038..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/xdh/XDHSpec.java +++ /dev/null @@ -1,34 +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", 256), - ; - - final String name; - final String curveName; - final int bitStrength; - - XDHSpec(@Nonnull String name, @Nonnull String curveName, int bitStrength) { - this.name = name; - this.curveName = curveName; - this.bitStrength = bitStrength; - } - - public String getName() { - return name; - } - - public String getCurveName() { - return curveName; - } - - public int getBitStrength() { - return bitStrength; - } -} 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 5fa71d46..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyAccessor.java +++ /dev/null @@ -1,148 +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 (e.g. "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() { - String primaryUserId = info.getPrimaryUserId(); - // If the key is located by Key ID, the algorithm of the primary User ID of the key provides the - // preferred symmetric algorithm. - PGPSignature signature = info.getLatestUserIdCertification(primaryUserId); - if (signature == null) { - signature = info.getLatestDirectKeySelfSignature(); - } - if (signature == null) { - throw new IllegalStateException("No valid signature found."); - } - return signature; - } - } - - public static class SubKey extends KeyAccessor { - - public SubKey(KeyRingInfo info, SubkeyIdentifier key) { - super(info, key); - } - - @Override - public @Nonnull PGPSignature getSignatureWithPreferences() { - PGPSignature signature; - if (key.getPrimaryKeyId() == key.getSubkeyId()) { - signature = info.getLatestDirectKeySelfSignature(); - if (signature == null) { - signature = info.getLatestUserIdCertification(info.getPrimaryUserId()); - } - } else { - signature = info.getCurrentSubkeyBindingSignature(key.getSubkeyId()); - } - - 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 f85c5c8a..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyInfo.java +++ /dev/null @@ -1,139 +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.asn1.gnu.GNUObjectIdentifiers; -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.requireFromId(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(); - - String curveName = ECUtil.getCurveName(identifier); - if (curveName != null) { - return curveName; - } - - // Workaround for ECUtil not recognizing ed25519 - // see https://github.com/bcgit/bc-java/issues/1087 - // UPDATE: Apparently 1087 is not fixed properly with BC 1.71 - // See https://github.com/bcgit/bc-java/issues/1142 - // TODO: Remove when BC 1.72 comes out with a fix. - if (identifier.equals(GNUObjectIdentifiers.Ed25519)) { - return EdDSACurve._Ed25519.getName(); - } - - return null; - } - - /** - * 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 fa4168dd..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java +++ /dev/null @@ -1,1185 +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.S2K; -import org.bouncycastle.bcpg.sig.PrimaryUserID; -import org.bouncycastle.bcpg.sig.RevocationReason; -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.RevocationState; -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; -import org.pgpainless.exception.KeyException; -import org.pgpainless.key.OpenPgpFingerprint; -import org.pgpainless.key.SubkeyIdentifier; -import org.pgpainless.key.util.RevocationAttributes; -import org.pgpainless.policy.Policy; -import org.pgpainless.signature.SignatureUtils; -import org.pgpainless.signature.consumer.SignaturePicker; -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_FROM_USERID = Pattern.compile("<([a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+)>"); - private static final Pattern PATTERN_EMAIL_EXPLICIT = Pattern.compile("^([a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+)$"); - - private final PGPKeyRing keys; - private final Signatures signatures; - private final Date referenceDate; - private final String primaryUserId; - private final RevocationState revocationState; - - /** - * 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 referenceDate date of validation - */ - public KeyRingInfo(PGPKeyRing keys, Date referenceDate) { - this(keys, PGPainless.getPolicy(), referenceDate); - } - - /** - * Evaluate the key ring at the provided validation date. - * - * @param keys key ring - * @param policy policy - * @param referenceDate validation date - */ - public KeyRingInfo(PGPKeyRing keys, Policy policy, Date referenceDate) { - this.referenceDate = referenceDate != null ? referenceDate : new Date(); - this.keys = keys; - this.signatures = new Signatures(keys, this.referenceDate, policy); - this.primaryUserId = findPrimaryUserId(); - this.revocationState = findRevocationState(); - } - - private RevocationState findRevocationState() { - PGPSignature revocation = signatures.primaryKeyRevocation; - if (revocation != null) { - return SignatureUtils.isHardRevocation(revocation) ? - RevocationState.hardRevoked() : RevocationState.softRevoked(revocation.getCreationTime()); - } - return RevocationState.notRevoked(); - } - - /** - * 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(OpenPgpFingerprint 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 { - // Key is soft-revoked, not yet re-bound - return SignatureUtils.isSignatureExpired(revocation) - || !revocation.getCreationTime().after(binding.getCreationTime()); - } - } - - 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(OpenPgpFingerprint 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 OpenPgpFingerprint} of this key ring. - * - * @return fingerprint - */ - public OpenPgpFingerprint getFingerprint() { - return OpenPgpFingerprint.of(getPublicKey()); - } - - public @Nullable String getPrimaryUserId() { - return primaryUserId; - } - - /** - * Return the current 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 user-id on the key, otherwise null. - * - * @return primary user-id or null - */ - private String findPrimaryUserId() { - String primaryUserId = null; - Date currentModificationDate = null; - - List userIds = getUserIds(); - if (userIds.isEmpty()) { - return null; - } - - String firstUserId = null; - for (String userId : userIds) { - PGPSignature certification = signatures.userIdCertifications.get(userId); - if (certification == null) { - continue; - } - - if (firstUserId == null) { - firstUserId = userId; - } - Date creationTime = certification.getCreationTime(); - - if (certification.getHashedSubPackets().isPrimaryUserID()) { - if (currentModificationDate == null || creationTime.after(currentModificationDate)) { - primaryUserId = userId; - currentModificationDate = creationTime; - } - - } - } - - if (primaryUserId != null) { - return primaryUserId; - } - - return firstUserId; - } - - /** - * 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 (isUserIdBound(userId)) { - valid.add(userId); - } - } - return valid; - } - - /** - * Return a list of all user-ids that were valid at some point, but might be expired by now. - * - * @return bound user-ids - */ - public List getValidAndExpiredUserIds() { - List probablyExpired = new ArrayList<>(); - List userIds = getUserIds(); - - for (String userId : userIds) { - PGPSignature certification = signatures.userIdCertifications.get(userId); - PGPSignature revocation = signatures.userIdRevocations.get(userId); - - // Unbound user-id - if (certification == null) { - continue; - } - - // Not revoked -> valid - if (revocation == null) { - probablyExpired.add(userId); - continue; - } - - // Hard revocation -> invalid - if (SignatureUtils.isHardRevocation(revocation)) { - continue; - } - - // Soft revocation -> valid if certification is newer than revocation (revalidation) - if (certification.getCreationTime().after(revocation.getCreationTime())) { - probablyExpired.add(userId); - } - } - return probablyExpired; - } - - /** - * 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) { - if (!userId.equals(primaryUserId)) { - if (!isUserIdBound(primaryUserId)) { - // primary user-id not valid - return false; - } - } - return isUserIdBound(userId); - } - - - private boolean isUserIdBound(String userId) { - - PGPSignature certification = signatures.userIdCertifications.get(userId); - PGPSignature revocation = signatures.userIdRevocations.get(userId); - - if (certification == null) { - return false; - } - if (SignatureUtils.isSignatureExpired(certification)) { - return false; - } - if (certification.getHashedSubPackets().isPrimaryUserID()) { - Date keyExpiration = SignatureSubpacketsUtil.getKeyExpirationTimeAsDate(certification, keys.getPublicKey()); - if (keyExpiration != null && referenceDate.after(keyExpiration)) { - 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_FROM_USERID.matcher(userId); - if (matcher.find()) { - emails.add(matcher.group(1)); - } else { - matcher = PATTERN_EMAIL_EXPLICIT.matcher(userId); - if (matcher.find()) { - emails.add(matcher.group(1)); - } - } - } - 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 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 - */ - @Nonnull - public PublicKeyAlgorithm getAlgorithm() { - return PublicKeyAlgorithm.requireFromId(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; - } - - public RevocationState getRevocationState() { - return revocationState; - } - - /** - * 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 getRevocationState().isSoftRevocation() ? getRevocationState().getDate() : null; - } - - /** - * 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(); - Date directKeyExpirationDate = null; - if (directKeySig != null) { - directKeyExpirationDate = SignatureSubpacketsUtil.getKeyExpirationTimeAsDate(directKeySig, getPublicKey()); - } - - PGPSignature primaryUserIdCertification = null; - Date userIdExpirationDate = null; - String possiblyExpiredPrimaryUserId = getPossiblyExpiredPrimaryUserId(); - if (possiblyExpiredPrimaryUserId != null) { - primaryUserIdCertification = getLatestUserIdCertification(possiblyExpiredPrimaryUserId); - if (primaryUserIdCertification != null) { - userIdExpirationDate = SignatureSubpacketsUtil.getKeyExpirationTimeAsDate(primaryUserIdCertification, getPublicKey()); - } - } - - if (directKeySig == null && primaryUserIdCertification == null) { - throw new NoSuchElementException("No direct-key signature and no user-id signature found."); - } - - if (directKeyExpirationDate != null && userIdExpirationDate == null) { - return directKeyExpirationDate; - } - - if (directKeyExpirationDate == null) { - return userIdExpirationDate; - } - - if (directKeyExpirationDate.before(userIdExpirationDate)) { - return directKeyExpirationDate; - } - - return userIdExpirationDate; - } - - public String getPossiblyExpiredPrimaryUserId() { - String validPrimaryUserId = getPrimaryUserId(); - if (validPrimaryUserId != null) { - return validPrimaryUserId; - } - - Date latestCreationTime = null; - String primaryUserId = null; - boolean foundPrimary = false; - for (String userId : getUserIds()) { - PGPSignature signature = getLatestUserIdCertification(userId); - if (signature == null) { - continue; - } - - boolean isPrimary = signature.getHashedSubPackets().isPrimaryUserID(); - if (foundPrimary && !isPrimary) { - continue; - } - - Date creationTime = signature.getCreationTime(); - if (latestCreationTime == null || creationTime.after(latestCreationTime) || isPrimary && !foundPrimary) { - latestCreationTime = creationTime; - primaryUserId = userId; - } - - foundPrimary |= isPrimary; - } - - return primaryUserId; - } - - /** - * Return the expiration date of the subkey with the provided fingerprint. - * - * @param fingerprint subkey fingerprint - * @return expiration date or null - */ - public @Nullable Date getSubkeyExpirationDate(OpenPgpFingerprint 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 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, e.g. {@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(OpenPgpFingerprint.of(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; - } - - public boolean isHardRevoked(String userId) { - PGPSignature revocation = signatures.userIdRevocations.get(userId); - if (revocation == null) { - return false; - } - RevocationReason revocationReason = revocation.getHashedSubPackets().getRevocationReason(); - return revocationReason == null || RevocationAttributes.Reason.isHardRevocation(revocationReason.getRevocationReason()); - } - - /** - * 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(OpenPgpFingerprint.of(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 ANY: - 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 KeyException.UnboundUserIdException( - OpenPgpFingerprint.of(keys), - 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; - } - - public Set getPreferredHashAlgorithms() { - return getPreferredHashAlgorithms(getPrimaryUserId()); - } - - public Set getPreferredHashAlgorithms(String userId) { - return getKeyAccessor(userId, getKeyId()).getPreferredHashAlgorithms(); - } - - public Set getPreferredHashAlgorithms(long keyId) { - return new KeyAccessor.SubKey(this, new SubkeyIdentifier(keys, keyId)) - .getPreferredHashAlgorithms(); - } - - public Set getPreferredSymmetricKeyAlgorithms() { - return getPreferredSymmetricKeyAlgorithms(getPrimaryUserId()); - } - - public Set getPreferredSymmetricKeyAlgorithms(String userId) { - return getKeyAccessor(userId, getKeyId()).getPreferredSymmetricKeyAlgorithms(); - } - - public Set getPreferredSymmetricKeyAlgorithms(long keyId) { - return new KeyAccessor.SubKey(this, new SubkeyIdentifier(keys, keyId)).getPreferredSymmetricKeyAlgorithms(); - } - - public Set getPreferredCompressionAlgorithms() { - return getPreferredCompressionAlgorithms(getPrimaryUserId()); - } - - public Set getPreferredCompressionAlgorithms(String userId) { - return getKeyAccessor(userId, getKeyId()).getPreferredCompressionAlgorithms(); - } - - public Set getPreferredCompressionAlgorithms(long keyId) { - return new KeyAccessor.SubKey(this, new SubkeyIdentifier(keys, keyId)).getPreferredCompressionAlgorithms(); - } - - /** - * Returns true, if the certificate has at least one usable encryption subkey. - * - * @return true if usable for encryption - */ - public boolean isUsableForEncryption() { - return isUsableForEncryption(EncryptionPurpose.ANY); - } - - /** - * Returns true, if the certificate has at least one usable encryption subkey for the given purpose. - * - * @param purpose purpose of encryption - * @return true if usable for encryption - */ - public boolean isUsableForEncryption(@Nonnull EncryptionPurpose purpose) { - return isKeyValidlyBound(getKeyId()) && !getEncryptionSubkeys(purpose).isEmpty(); - } - - /** - * Returns true, if the key ring is capable of signing. - * Contrary to {@link #isUsableForSigning()}, this method also returns true, if this {@link KeyRingInfo} is based - * on a key ring which has at least one valid public key marked for signing. - * The secret key is not required for the key ring to qualify as signing capable. - * - * @return true if key corresponding to the cert is capable of signing - */ - public boolean isSigningCapable() { - // check if primary-key is revoked / expired - if (!isKeyValidlyBound(getKeyId())) { - return false; - } - // check if it has signing-capable key - return !getSigningSubkeys().isEmpty(); - } - - /** - * Returns true, if this {@link KeyRingInfo} is based on a {@link PGPSecretKeyRing}, which has a valid signing key - * which is ready to be used (i.e. secret key is present and is not on a smart-card). - * - * If you just want to check, whether a key / certificate has signing capable subkeys, - * use {@link #isSigningCapable()} instead. - * - * @return true if key is ready to be used for signing - */ - public boolean isUsableForSigning() { - if (!isSigningCapable()) { - return false; - } - - List signingKeys = getSigningSubkeys(); - for (PGPPublicKey pk : signingKeys) { - return isSecretKeyAvailable(pk.getKeyID()); - } - // No usable secret key found - return false; - } - - public boolean isSecretKeyAvailable(long keyId) { - PGPSecretKey sk = getSecretKey(keyId); - if (sk == null) { - // Missing secret key - return false; - } - S2K s2K = sk.getS2K(); - // Unencrypted key - if (s2K == null) { - return true; - } - - // Secret key on smart-card - int s2kType = s2K.getType(); - if (s2kType >= 100 && s2kType <= 110) { - return false; - } - // protected secret key - return true; - } - - private KeyAccessor getKeyAccessor(@Nullable String userId, long keyID) { - if (getPublicKey(keyID) == null) { - throw new NoSuchElementException("No subkey with key id " + Long.toHexString(keyID) + " found on this key."); - } - if (userId != null && !getUserIds().contains(userId)) { - throw new NoSuchElementException("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 referenceDate, Policy policy) { - primaryKeyRevocation = SignaturePicker.pickCurrentRevocationSelfSignature(keyRing, policy, referenceDate); - primaryKeySelfSignature = SignaturePicker.pickLatestDirectKeySignature(keyRing, policy, referenceDate); - 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, referenceDate); - if (revocation != null) { - userIdRevocations.put(userId, revocation); - } - PGPSignature certification = SignaturePicker.pickLatestUserIdCertificationSignature(keyRing, userId, policy, referenceDate); - 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, referenceDate); - if (subkeyRevocation != null) { - subkeyRevocations.put(subkey.getKeyID(), subkeyRevocation); - } - PGPSignature subkeyBinding = SignaturePicker.pickLatestSubkeyBindingSignature(keyRing, subkey, policy, referenceDate); - 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 01c09903..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java +++ /dev/null @@ -1,859 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.modification.secretkeyring; - -import org.bouncycastle.bcpg.S2K; -import org.bouncycastle.bcpg.SecretKeyPacket; -import org.bouncycastle.bcpg.sig.KeyExpirationTime; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPKeyPair; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; -import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; -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.PublicKeyAlgorithm; -import org.pgpainless.algorithm.SignatureType; -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; -import org.pgpainless.algorithm.negotiation.HashAlgorithmNegotiator; -import org.pgpainless.implementation.ImplementationFactory; -import org.pgpainless.key.generation.KeyRingBuilder; -import org.pgpainless.key.generation.KeySpec; -import org.pgpainless.key.info.KeyRingInfo; -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.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.builder.DirectKeySelfSignatureBuilder; -import org.pgpainless.signature.builder.PrimaryKeyBindingSignatureBuilder; -import org.pgpainless.signature.builder.RevocationSignatureBuilder; -import org.pgpainless.signature.builder.SelfSignatureBuilder; -import org.pgpainless.signature.builder.SubkeyBindingSignatureBuilder; -import org.pgpainless.signature.subpackets.RevocationSignatureSubpackets; -import org.pgpainless.signature.subpackets.SelfSignatureSubpackets; -import org.pgpainless.signature.subpackets.SignatureSubpackets; -import org.pgpainless.signature.subpackets.SignatureSubpacketsHelper; -import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil; -import org.pgpainless.util.Passphrase; -import org.pgpainless.util.selection.userid.SelectUserId; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -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.Iterator; -import java.util.List; -import java.util.Map; -import java.util.NoSuchElementException; -import java.util.Set; - -import static org.pgpainless.util.CollectionUtils.concat; - -public class SecretKeyRingEditor implements SecretKeyRingEditorInterface { - - private PGPSecretKeyRing secretKeyRing; - private final Date referenceTime; - - public SecretKeyRingEditor(PGPSecretKeyRing secretKeyRing) { - this(secretKeyRing, null); - } - - public SecretKeyRingEditor(PGPSecretKeyRing secretKeyRing, Date referenceTime) { - if (secretKeyRing == null) { - throw new NullPointerException("SecretKeyRing MUST NOT be null."); - } - this.secretKeyRing = secretKeyRing; - this.referenceTime = referenceTime; - } - - @Override - public SecretKeyRingEditorInterface addUserId( - @Nonnull CharSequence userId, - @Nonnull SecretKeyRingProtector secretKeyRingProtector) - throws PGPException { - return addUserId(userId, null, secretKeyRingProtector); - } - - @Override - public SecretKeyRingEditorInterface addUserId( - @Nonnull CharSequence userId, - @Nullable SelfSignatureSubpackets.Callback signatureSubpacketCallback, - @Nonnull SecretKeyRingProtector protector) - throws PGPException { - String sanitizeUserId = sanitizeUserId(userId); - - // user-id certifications live on the primary key - PGPSecretKey primaryKey = secretKeyRing.getSecretKey(); - - // retain key flags from previous signature - KeyRingInfo info = PGPainless.inspectKeyRing(secretKeyRing, referenceTime); - if (info.isHardRevoked(userId.toString())) { - throw new IllegalArgumentException("User-ID " + userId + " is hard revoked and cannot be re-certified."); - } - List keyFlags = info.getKeyFlagsOf(info.getKeyId()); - - Set hashAlgorithmPreferences; - Set symmetricKeyAlgorithmPreferences; - Set compressionAlgorithmPreferences; - try { - hashAlgorithmPreferences = info.getPreferredHashAlgorithms(); - symmetricKeyAlgorithmPreferences = info.getPreferredSymmetricKeyAlgorithms(); - compressionAlgorithmPreferences = info.getPreferredCompressionAlgorithms(); - } catch (IllegalStateException e) { - // missing user-id sig - AlgorithmSuite algorithmSuite = AlgorithmSuite.getDefaultAlgorithmSuite(); - hashAlgorithmPreferences = algorithmSuite.getHashAlgorithms(); - symmetricKeyAlgorithmPreferences = algorithmSuite.getSymmetricKeyAlgorithms(); - compressionAlgorithmPreferences = algorithmSuite.getCompressionAlgorithms(); - } - - SelfSignatureBuilder builder = new SelfSignatureBuilder(primaryKey, protector); - if (referenceTime != null) { - builder.getHashedSubpackets().setSignatureCreationTime(referenceTime); - } - builder.setSignatureType(SignatureType.POSITIVE_CERTIFICATION); - - // Retain signature subpackets of previous signatures - builder.getHashedSubpackets().setKeyFlags(keyFlags); - builder.getHashedSubpackets().setPreferredHashAlgorithms(hashAlgorithmPreferences); - builder.getHashedSubpackets().setPreferredSymmetricKeyAlgorithms(symmetricKeyAlgorithmPreferences); - builder.getHashedSubpackets().setPreferredCompressionAlgorithms(compressionAlgorithmPreferences); - builder.getHashedSubpackets().setFeatures(Feature.MODIFICATION_DETECTION); - - builder.applyCallback(signatureSubpacketCallback); - - PGPSignature signature = builder.build(primaryKey.getPublicKey(), sanitizeUserId); - secretKeyRing = KeyRingUtils.injectCertification(secretKeyRing, sanitizeUserId, signature); - - return this; - } - - @Override - public SecretKeyRingEditorInterface addPrimaryUserId( - @Nonnull CharSequence userId, @Nonnull SecretKeyRingProtector protector) - throws PGPException { - - // Determine previous key expiration date - PGPPublicKey primaryKey = secretKeyRing.getSecretKey().getPublicKey(); - KeyRingInfo info = PGPainless.inspectKeyRing(secretKeyRing, referenceTime); - String primaryUserId = info.getPrimaryUserId(); - PGPSignature signature = primaryUserId == null ? - info.getLatestDirectKeySelfSignature() : info.getLatestUserIdCertification(primaryUserId); - final Date previousKeyExpiration = signature == null ? null : - SignatureSubpacketsUtil.getKeyExpirationTimeAsDate(signature, primaryKey); - - // Add new primary user-id signature - addUserId( - userId, - new SelfSignatureSubpackets.Callback() { - @Override - public void modifyHashedSubpackets(SelfSignatureSubpackets hashedSubpackets) { - hashedSubpackets.setPrimaryUserId(); - if (previousKeyExpiration != null) { - hashedSubpackets.setKeyExpirationTime(primaryKey, previousKeyExpiration); - } else { - hashedSubpackets.setKeyExpirationTime(null); - } - } - }, - protector); - - // unmark previous primary user-ids to be non-primary - info = PGPainless.inspectKeyRing(secretKeyRing, referenceTime); - for (String otherUserId : info.getValidAndExpiredUserIds()) { - if (userId.toString().equals(otherUserId)) { - continue; - } - - // We need to unmark this user-id as primary - if (info.getLatestUserIdCertification(otherUserId).getHashedSubPackets().isPrimaryUserID()) { - addUserId(otherUserId, new SelfSignatureSubpackets.Callback() { - @Override - public void modifyHashedSubpackets(SelfSignatureSubpackets hashedSubpackets) { - hashedSubpackets.setPrimaryUserId(null); - hashedSubpackets.setKeyExpirationTime(null); // non-primary - } - }, protector); - } - } - return this; - } - - @Override - public SecretKeyRingEditorInterface removeUserId( - SelectUserId userIdSelector, - SecretKeyRingProtector protector) - throws PGPException { - RevocationAttributes revocationAttributes = RevocationAttributes.createCertificateRevocation() - .withReason(RevocationAttributes.Reason.USER_ID_NO_LONGER_VALID) - .withoutDescription(); - return revokeUserIds(userIdSelector, - protector, - revocationAttributes); - } - - @Override - public SecretKeyRingEditorInterface removeUserId( - CharSequence userId, - SecretKeyRingProtector protector) - throws PGPException { - return removeUserId( - SelectUserId.exactMatch(userId.toString()), - protector); - } - - @Override - public SecretKeyRingEditorInterface replaceUserId(@Nonnull CharSequence oldUserId, - @Nonnull CharSequence newUserId, - @Nonnull SecretKeyRingProtector protector) - throws PGPException { - String oldUID = oldUserId.toString().trim(); - String newUID = newUserId.toString().trim(); - if (oldUID.isEmpty()) { - throw new IllegalArgumentException("Old user-id cannot be empty."); - } - - if (newUID.isEmpty()) { - throw new IllegalArgumentException("New user-id cannot be empty."); - } - - KeyRingInfo info = PGPainless.inspectKeyRing(secretKeyRing, referenceTime); - if (!info.isUserIdValid(oldUID)) { - throw new NoSuchElementException("Key does not carry user-id '" + oldUID + "', or it is not valid."); - } - - PGPSignature oldCertification = info.getLatestUserIdCertification(oldUID); - if (oldCertification == null) { - throw new AssertionError("Certification for old user-id MUST NOT be null."); - } - - // Bind new user-id - addUserId(newUserId, new SelfSignatureSubpackets.Callback() { - @Override - public void modifyHashedSubpackets(SelfSignatureSubpackets hashedSubpackets) { - SignatureSubpacketsHelper.applyFrom(oldCertification.getHashedSubPackets(), (SignatureSubpackets) hashedSubpackets); - // Primary user-id - if (oldUID.equals(info.getPrimaryUserId())) { - // Implicit primary user-id - if (!oldCertification.getHashedSubPackets().isPrimaryUserID()) { - hashedSubpackets.setPrimaryUserId(); - } - } - } - - @Override - public void modifyUnhashedSubpackets(SelfSignatureSubpackets unhashedSubpackets) { - SignatureSubpacketsHelper.applyFrom(oldCertification.getUnhashedSubPackets(), (SignatureSubpackets) unhashedSubpackets); - } - }, protector); - - return revokeUserId(oldUID, protector); - } - - // TODO: Move to utility class? - private String sanitizeUserId(@Nonnull CharSequence userId) { - // TODO: Further research how to sanitize user IDs. - // eg. what about newlines? - return userId.toString().trim(); - } - - @Override - public SecretKeyRingEditorInterface addSubKey( - @Nonnull KeySpec keySpec, - @Nonnull Passphrase subKeyPassphrase, - @Nonnull SecretKeyRingProtector secretKeyRingProtector) - throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException, IOException { - PGPKeyPair keyPair = KeyRingBuilder.generateKeyPair(keySpec); - - SecretKeyRingProtector subKeyProtector = PasswordBasedSecretKeyRingProtector - .forKeyId(keyPair.getKeyID(), subKeyPassphrase); - - SelfSignatureSubpackets.Callback callback = new SelfSignatureSubpackets.Callback() { - @Override - public void modifyHashedSubpackets(SelfSignatureSubpackets hashedSubpackets) { - SignatureSubpacketsHelper.applyFrom(keySpec.getSubpackets(), (SignatureSubpackets) hashedSubpackets); - } - }; - - List keyFlags = KeyFlag.fromBitmask(keySpec.getSubpackets().getKeyFlags()); - KeyFlag firstFlag = keyFlags.remove(0); - KeyFlag[] otherFlags = keyFlags.toArray(new KeyFlag[0]); - - return addSubKey(keyPair, callback, subKeyProtector, secretKeyRingProtector, firstFlag, otherFlags); - } - - @Override - public SecretKeyRingEditorInterface addSubKey( - @Nonnull KeySpec keySpec, - @Nullable Passphrase subkeyPassphrase, - @Nullable SelfSignatureSubpackets.Callback subpacketsCallback, - @Nonnull SecretKeyRingProtector secretKeyRingProtector) - throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { - PGPKeyPair keyPair = KeyRingBuilder.generateKeyPair(keySpec); - - SecretKeyRingProtector subKeyProtector = PasswordBasedSecretKeyRingProtector - .forKeyId(keyPair.getKeyID(), subkeyPassphrase); - - List keyFlags = KeyFlag.fromBitmask(keySpec.getSubpackets().getKeyFlags()); - KeyFlag firstFlag = keyFlags.remove(0); - KeyFlag[] otherFlags = keyFlags.toArray(new KeyFlag[0]); - - return addSubKey(keyPair, subpacketsCallback, subKeyProtector, secretKeyRingProtector, firstFlag, otherFlags); - } - - @Override - public SecretKeyRingEditorInterface addSubKey( - @Nonnull PGPKeyPair subkey, - @Nullable SelfSignatureSubpackets.Callback bindingSignatureCallback, - @Nonnull SecretKeyRingProtector subkeyProtector, - @Nonnull SecretKeyRingProtector primaryKeyProtector, - @Nonnull KeyFlag keyFlag, - KeyFlag... additionalKeyFlags) - throws PGPException, IOException { - KeyFlag[] flags = concat(keyFlag, additionalKeyFlags); - PublicKeyAlgorithm subkeyAlgorithm = PublicKeyAlgorithm.requireFromId(subkey.getPublicKey().getAlgorithm()); - SignatureSubpacketsUtil.assureKeyCanCarryFlags(subkeyAlgorithm); - - // check key against public key algorithm policy - PublicKeyAlgorithm publicKeyAlgorithm = PublicKeyAlgorithm.requireFromId(subkey.getPublicKey().getAlgorithm()); - int bitStrength = subkey.getPublicKey().getBitStrength(); - if (!PGPainless.getPolicy().getPublicKeyAlgorithmPolicy().isAcceptable(publicKeyAlgorithm, bitStrength)) { - throw new IllegalArgumentException("Public key algorithm policy violation: " + - publicKeyAlgorithm + " with bit strength " + bitStrength + " is not acceptable."); - } - - PGPSecretKey primaryKey = secretKeyRing.getSecretKey(); - KeyRingInfo info = PGPainless.inspectKeyRing(secretKeyRing, referenceTime); - HashAlgorithm hashAlgorithm = HashAlgorithmNegotiator - .negotiateSignatureHashAlgorithm(PGPainless.getPolicy()) - .negotiateHashAlgorithm(info.getPreferredHashAlgorithms()); - - PGPSecretKey secretSubkey = new PGPSecretKey(subkey.getPrivateKey(), subkey.getPublicKey(), ImplementationFactory.getInstance() - .getV4FingerprintCalculator(), false, subkeyProtector.getEncryptor(subkey.getKeyID())); - - SubkeyBindingSignatureBuilder skBindingBuilder = new SubkeyBindingSignatureBuilder(primaryKey, primaryKeyProtector, hashAlgorithm); - if (referenceTime != null) { - skBindingBuilder.getHashedSubpackets().setSignatureCreationTime(referenceTime); - } - skBindingBuilder.getHashedSubpackets().setKeyFlags(flags); - - if (subkeyAlgorithm.isSigningCapable()) { - PrimaryKeyBindingSignatureBuilder pkBindingBuilder = new PrimaryKeyBindingSignatureBuilder(secretSubkey, subkeyProtector, hashAlgorithm); - if (referenceTime != null) { - pkBindingBuilder.getHashedSubpackets().setSignatureCreationTime(referenceTime); - } - PGPSignature pkBinding = pkBindingBuilder.build(primaryKey.getPublicKey()); - skBindingBuilder.getHashedSubpackets().addEmbeddedSignature(pkBinding); - } - - skBindingBuilder.applyCallback(bindingSignatureCallback); - PGPSignature skBinding = skBindingBuilder.build(secretSubkey.getPublicKey()); - - secretSubkey = KeyRingUtils.secretKeyPlusSignature(secretSubkey, skBinding); - secretKeyRing = KeyRingUtils.keysPlusSecretKey(secretKeyRing, secretSubkey); - return this; - } - - @Override - public SecretKeyRingEditorInterface revoke(@Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationAttributes revocationAttributes) - throws PGPException { - RevocationSignatureSubpackets.Callback callback = callbackFromRevocationAttributes(revocationAttributes); - return revoke(secretKeyRingProtector, callback); - } - - @Override - public SecretKeyRingEditorInterface revoke(@Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationSignatureSubpackets.Callback subpacketsCallback) - throws PGPException { - return revokeSubKey(secretKeyRing.getSecretKey().getKeyID(), secretKeyRingProtector, subpacketsCallback); - } - - @Override - public SecretKeyRingEditorInterface revokeSubKey(long subKeyId, - SecretKeyRingProtector protector, - RevocationAttributes revocationAttributes) - throws PGPException { - RevocationSignatureSubpackets.Callback callback = callbackFromRevocationAttributes(revocationAttributes); - return revokeSubKey(subKeyId, protector, callback); - } - - @Override - public SecretKeyRingEditorInterface revokeSubKey(long keyID, - @Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationSignatureSubpackets.Callback subpacketsCallback) - throws PGPException { - // retrieve subkey to be revoked - PGPPublicKey revokeeSubKey = KeyRingUtils.requirePublicKeyFrom(secretKeyRing, keyID); - // create revocation - PGPSignature subKeyRevocation = generateRevocation(secretKeyRingProtector, revokeeSubKey, - subpacketsCallback); - // inject revocation sig into key ring - secretKeyRing = KeyRingUtils.injectCertification(secretKeyRing, revokeeSubKey, subKeyRevocation); - return this; - } - - @Override - public PGPSignature createRevocationCertificate(@Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationAttributes revocationAttributes) - throws PGPException { - PGPPublicKey revokeeSubKey = secretKeyRing.getPublicKey(); - PGPSignature revocationCertificate = generateRevocation( - secretKeyRingProtector, revokeeSubKey, callbackFromRevocationAttributes(revocationAttributes)); - return revocationCertificate; - } - - @Override - public PGPSignature createRevocationCertificate( - long subkeyId, - @Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationAttributes revocationAttributes) - throws PGPException { - PGPPublicKey revokeeSubkey = KeyRingUtils.requirePublicKeyFrom(secretKeyRing, subkeyId); - RevocationSignatureSubpackets.Callback callback = callbackFromRevocationAttributes(revocationAttributes); - return generateRevocation(secretKeyRingProtector, revokeeSubkey, callback); - } - - @Override - public PGPSignature createRevocationCertificate( - long subkeyId, - @Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationSignatureSubpackets.Callback certificateSubpacketsCallback) - throws PGPException { - PGPPublicKey revokeeSubkey = KeyRingUtils.requirePublicKeyFrom(secretKeyRing, subkeyId); - return generateRevocation(secretKeyRingProtector, revokeeSubkey, certificateSubpacketsCallback); - } - - private PGPSignature generateRevocation(@Nonnull SecretKeyRingProtector protector, - @Nonnull PGPPublicKey revokeeSubKey, - @Nullable RevocationSignatureSubpackets.Callback callback) - throws PGPException { - PGPSecretKey primaryKey = secretKeyRing.getSecretKey(); - SignatureType signatureType = revokeeSubKey.isMasterKey() ? - SignatureType.KEY_REVOCATION : SignatureType.SUBKEY_REVOCATION; - - RevocationSignatureBuilder signatureBuilder = - new RevocationSignatureBuilder(signatureType, primaryKey, protector); - signatureBuilder.applyCallback(callback); - PGPSignature revocation = signatureBuilder.build(revokeeSubKey); - return revocation; - } - - private static RevocationSignatureSubpackets.Callback callbackFromRevocationAttributes( - @Nullable RevocationAttributes attributes) { - return new RevocationSignatureSubpackets.Callback() { - @Override - public void modifyHashedSubpackets(RevocationSignatureSubpackets hashedSubpackets) { - if (attributes != null) { - hashedSubpackets.setRevocationReason(attributes); - } - } - }; - } - - @Override - public SecretKeyRingEditorInterface revokeUserId( - @Nonnull CharSequence userId, - @Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationAttributes revocationAttributes) - throws PGPException { - 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"); - } - } - - RevocationSignatureSubpackets.Callback callback = new RevocationSignatureSubpackets.Callback() { - @Override - public void modifyHashedSubpackets(RevocationSignatureSubpackets hashedSubpackets) { - if (revocationAttributes != null) { - hashedSubpackets.setRevocationReason(false, revocationAttributes); - } - } - }; - - return revokeUserId(userId, secretKeyRingProtector, callback); - } - - @Override - public SecretKeyRingEditorInterface revokeUserId( - @Nonnull CharSequence userId, - @Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationSignatureSubpackets.Callback subpacketCallback) - throws PGPException { - String sanitized = sanitizeUserId(userId); - return revokeUserIds( - SelectUserId.exactMatch(sanitized), - secretKeyRingProtector, - subpacketCallback); - } - - @Override - public SecretKeyRingEditorInterface revokeUserIds( - @Nonnull SelectUserId userIdSelector, - @Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationAttributes revocationAttributes) - throws PGPException { - - return revokeUserIds( - userIdSelector, - secretKeyRingProtector, - new RevocationSignatureSubpackets.Callback() { - @Override - public void modifyHashedSubpackets(RevocationSignatureSubpackets hashedSubpackets) { - hashedSubpackets.setRevocationReason(revocationAttributes); - } - }); - } - - @Override - public SecretKeyRingEditorInterface revokeUserIds( - @Nonnull SelectUserId userIdSelector, - @Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationSignatureSubpackets.Callback subpacketsCallback) - throws PGPException { - List selected = userIdSelector.selectUserIds(secretKeyRing); - if (selected.isEmpty()) { - throw new NoSuchElementException("No matching user-ids found on the key."); - } - - for (String userId : selected) { - doRevokeUserId(userId, secretKeyRingProtector, subpacketsCallback); - } - - return this; - } - - private SecretKeyRingEditorInterface doRevokeUserId( - @Nonnull String userId, - @Nonnull SecretKeyRingProtector protector, - @Nullable RevocationSignatureSubpackets.Callback callback) - throws PGPException { - PGPSecretKey primarySecretKey = secretKeyRing.getSecretKey(); - RevocationSignatureBuilder signatureBuilder = new RevocationSignatureBuilder( - SignatureType.CERTIFICATION_REVOCATION, - primarySecretKey, - protector); - if (referenceTime != null) { - signatureBuilder.getHashedSubpackets().setSignatureCreationTime(referenceTime); - } - - signatureBuilder.applyCallback(callback); - - PGPSignature revocationSignature = signatureBuilder.build(userId); - secretKeyRing = KeyRingUtils.injectCertification(secretKeyRing, userId, revocationSignature); - return this; - } - - @Override - public SecretKeyRingEditorInterface setExpirationDate( - @Nullable Date expiration, - @Nonnull SecretKeyRingProtector secretKeyRingProtector) - throws PGPException { - - PGPSecretKey primaryKey = secretKeyRing.getSecretKey(); - if (!primaryKey.isMasterKey()) { - throw new IllegalArgumentException("Key Ring does not appear to contain a primary secret key."); - } - - // reissue direct key sig - PGPSignature prevDirectKeySig = getPreviousDirectKeySignature(); - if (prevDirectKeySig != null) { - PGPSignature directKeySig = reissueDirectKeySignature(expiration, secretKeyRingProtector, prevDirectKeySig); - secretKeyRing = KeyRingUtils.injectCertification(secretKeyRing, primaryKey.getPublicKey(), directKeySig); - } - - // reissue primary user-id sig - String primaryUserId = PGPainless.inspectKeyRing(secretKeyRing, referenceTime).getPossiblyExpiredPrimaryUserId(); - if (primaryUserId != null) { - PGPSignature prevUserIdSig = getPreviousUserIdSignatures(primaryUserId); - PGPSignature userIdSig = reissuePrimaryUserIdSig(expiration, secretKeyRingProtector, primaryUserId, prevUserIdSig); - secretKeyRing = KeyRingUtils.injectCertification(secretKeyRing, primaryUserId, userIdSig); - } - - KeyRingInfo info = PGPainless.inspectKeyRing(secretKeyRing, referenceTime); - for (String userId : info.getValidUserIds()) { - if (userId.equals(primaryUserId)) { - continue; - } - - PGPSignature prevUserIdSig = info.getLatestUserIdCertification(userId); - if (prevUserIdSig == null) { - throw new AssertionError("A valid user-id shall never have no user-id signature."); - } - - if (prevUserIdSig.getHashedSubPackets().isPrimaryUserID()) { - PGPSignature userIdSig = reissueNonPrimaryUserId(secretKeyRingProtector, userId, prevUserIdSig); - secretKeyRing = KeyRingUtils.injectCertification(secretKeyRing, primaryUserId, userIdSig); - } - } - - return this; - } - - private PGPSignature reissueNonPrimaryUserId( - SecretKeyRingProtector secretKeyRingProtector, - String userId, - PGPSignature prevUserIdSig) - throws PGPException { - SelfSignatureBuilder builder = new SelfSignatureBuilder(secretKeyRing.getSecretKey(), secretKeyRingProtector, prevUserIdSig); - if (referenceTime != null) { - builder.getHashedSubpackets().setSignatureCreationTime(referenceTime); - } - builder.applyCallback(new SelfSignatureSubpackets.Callback() { - @Override - public void modifyHashedSubpackets(SelfSignatureSubpackets hashedSubpackets) { - // unmark as primary - hashedSubpackets.setPrimaryUserId(null); - } - }); - return builder.build(secretKeyRing.getPublicKey(), userId); - } - - private PGPSignature reissuePrimaryUserIdSig( - @Nullable Date expiration, - @Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nonnull String primaryUserId, - @Nonnull PGPSignature prevUserIdSig) - throws PGPException { - PGPSecretKey primaryKey = secretKeyRing.getSecretKey(); - PGPPublicKey publicKey = primaryKey.getPublicKey(); - - SelfSignatureBuilder builder = new SelfSignatureBuilder(primaryKey, secretKeyRingProtector, prevUserIdSig); - if (referenceTime != null) { - builder.getHashedSubpackets().setSignatureCreationTime(referenceTime); - } - builder.applyCallback(new SelfSignatureSubpackets.Callback() { - @Override - public void modifyHashedSubpackets(SelfSignatureSubpackets hashedSubpackets) { - if (expiration != null) { - hashedSubpackets.setKeyExpirationTime(true, publicKey.getCreationTime(), expiration); - } else { - hashedSubpackets.setKeyExpirationTime(new KeyExpirationTime(true, 0)); - } - hashedSubpackets.setPrimaryUserId(); - } - }); - return builder.build(publicKey, primaryUserId); - } - - private PGPSignature reissueDirectKeySignature( - Date expiration, - SecretKeyRingProtector secretKeyRingProtector, - PGPSignature prevDirectKeySig) - throws PGPException { - PGPSecretKey primaryKey = secretKeyRing.getSecretKey(); - PGPPublicKey publicKey = primaryKey.getPublicKey(); - final Date keyCreationTime = publicKey.getCreationTime(); - - DirectKeySelfSignatureBuilder builder = new DirectKeySelfSignatureBuilder(primaryKey, secretKeyRingProtector, prevDirectKeySig); - if (referenceTime != null) { - builder.getHashedSubpackets().setSignatureCreationTime(referenceTime); - } - builder.applyCallback(new SelfSignatureSubpackets.Callback() { - @Override - public void modifyHashedSubpackets(SelfSignatureSubpackets hashedSubpackets) { - if (expiration != null) { - hashedSubpackets.setKeyExpirationTime(keyCreationTime, expiration); - } else { - hashedSubpackets.setKeyExpirationTime(null); - } - } - }); - - return builder.build(publicKey); - } - - private PGPSignature getPreviousDirectKeySignature() { - KeyRingInfo info = PGPainless.inspectKeyRing(secretKeyRing, referenceTime); - return info.getLatestDirectKeySelfSignature(); - } - - private PGPSignature getPreviousUserIdSignatures(String userId) { - KeyRingInfo info = PGPainless.inspectKeyRing(secretKeyRing, referenceTime); - return info.getLatestUserIdCertification(userId); - } - - @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 7014d518..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.java +++ /dev/null @@ -1,624 +0,0 @@ -// SPDX-FileCopyrightText: 2020 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.Date; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPKeyPair; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.PGPSignature; -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; - -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 - * - * @throws PGPException in case we cannot generate a signature for the user-id - */ - SecretKeyRingEditorInterface addUserId( - @Nonnull CharSequence userId, - @Nonnull SecretKeyRingProtector secretKeyRingProtector) - throws PGPException; - - /** - * Add a user-id to the key ring. - * - * @param userId user-id - * @param signatureSubpacketCallback callback that can be used to modify signature subpackets of the - * certification signature. - * @param protector protector to unlock the primary secret key - * @return the builder - * - * @throws PGPException in case we cannot generate a signature for the user-id - */ - SecretKeyRingEditorInterface addUserId( - @Nonnull CharSequence userId, - @Nullable SelfSignatureSubpackets.Callback signatureSubpacketCallback, - @Nonnull SecretKeyRingProtector protector) - throws PGPException; - - /** - * 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 - */ - SecretKeyRingEditorInterface addPrimaryUserId( - @Nonnull CharSequence userId, - @Nonnull SecretKeyRingProtector protector) - throws PGPException; - - /** - * Convenience method to revoke selected user-ids using soft revocation signatures. - * The revocation will use {@link RevocationAttributes.Reason#USER_ID_NO_LONGER_VALID}, so that the user-id - * can be re-certified at a later point. - * - * @param userIdSelector 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 - */ - SecretKeyRingEditorInterface removeUserId(SelectUserId userIdSelector, - SecretKeyRingProtector protector) - throws PGPException; - - /** - * Convenience method to revoke a single user-id using a soft revocation signature. - * The revocation will use {@link 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 - */ - SecretKeyRingEditorInterface removeUserId(CharSequence userId, - SecretKeyRingProtector protector) - throws PGPException; - - /** - * 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 {@link 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 - */ - SecretKeyRingEditorInterface replaceUserId(CharSequence oldUserId, - CharSequence newUserId, - SecretKeyRingProtector protector) - throws PGPException; - - /** - * 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 - * - * @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 - */ - SecretKeyRingEditorInterface addSubKey( - @Nonnull KeySpec keySpec, - @Nonnull Passphrase subKeyPassphrase, - @Nonnull SecretKeyRingProtector secretKeyRingProtector) - throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException, IOException; - - /** - * Add a subkey to the key ring. - * The subkey will be generated from the provided {@link KeySpec}. - * - * @param keySpec key spec of the subkey - * @param subkeyPassphrase passphrase to encrypt the subkey - * @param subpacketsCallback callback to modify the subpackets of the subkey binding signature - * @param secretKeyRingProtector protector to unlock the primary key - * @return 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 - */ - SecretKeyRingEditorInterface addSubKey( - @Nonnull KeySpec keySpec, - @Nonnull Passphrase subkeyPassphrase, - @Nullable SelfSignatureSubpackets.Callback subpacketsCallback, - @Nonnull SecretKeyRingProtector secretKeyRingProtector) - throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException; - - /** - * Add a subkey to the key ring. - * - * @param subkey subkey key pair - * @param bindingSignatureCallback 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 key flag for the subkey - * @param additionalKeyFlags 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 NoSuchAlgorithmException in case of missing algorithm support in the crypto backend - */ - SecretKeyRingEditorInterface addSubKey( - @Nonnull PGPKeyPair subkey, - @Nullable SelfSignatureSubpackets.Callback bindingSignatureCallback, - @Nonnull SecretKeyRingProtector subkeyProtector, - @Nonnull SecretKeyRingProtector primaryKeyProtector, - @Nonnull KeyFlag keyFlag, - KeyFlag... additionalKeyFlags) - throws PGPException, IOException, NoSuchAlgorithmException; - - /** - * 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 - * - * @throws PGPException in case we cannot generate a revocation signature - */ - default SecretKeyRingEditorInterface revoke( - @Nonnull SecretKeyRingProtector secretKeyRingProtector) - throws PGPException { - return revoke(secretKeyRingProtector, (RevocationAttributes) null); - } - - /** - * Revoke the key ring using the provided revocation attributes. - * The attributes define, whether the revocation was a hard revocation or not. - * - * @param secretKeyRingProtector protector of the primary key - * @param revocationAttributes reason for the revocation - * @return the builder - * - * @throws PGPException in case we cannot generate a revocation signature - */ - SecretKeyRingEditorInterface revoke( - @Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationAttributes revocationAttributes) - throws PGPException; - - /** - * Revoke the key ring. - * You can use the {@link RevocationSignatureSubpackets.Callback} to modify the revocation signatures - * subpackets, e.g. in order to define whether this is a hard or soft revocation. - * - * @param secretKeyRingProtector protector to unlock the primary secret key - * @param subpacketsCallback callback to modify the revocations subpackets - * @return builder - * - * @throws PGPException in case we cannot generate a revocation signature - */ - SecretKeyRingEditorInterface revoke( - @Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationSignatureSubpackets.Callback subpacketsCallback) 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. - * - * Note: This method will hard-revoke the provided subkey, meaning it cannot be re-certified at a later point. - * If you instead want to temporarily "deactivate" the subkey, provide a soft revocation reason, - * e.g. by calling {@link #revokeSubKey(OpenPgpFingerprint, SecretKeyRingProtector, RevocationAttributes)} - * and provide a suitable {@link RevocationAttributes} object. - * - * @param fingerprint fingerprint of the subkey to be revoked - * @param secretKeyRingProtector protector to unlock the secret key ring - * @return the builder - * - * @throws PGPException in case we cannot generate a revocation signature for the subkey - */ - default SecretKeyRingEditorInterface revokeSubKey( - @Nonnull OpenPgpFingerprint fingerprint, - @Nonnull 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 - * - * @throws PGPException in case we cannot generate a revocation signature for the subkey - */ - default SecretKeyRingEditorInterface revokeSubKey( - OpenPgpFingerprint fingerprint, - SecretKeyRingProtector secretKeyRingProtector, - RevocationAttributes revocationAttributes) - throws PGPException { - return revokeSubKey(fingerprint.getKeyId(), - secretKeyRingProtector, - 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 {@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 - * - * @throws PGPException in case we cannot generate a revocation signature for the subkey - */ - SecretKeyRingEditorInterface revokeSubKey( - long subKeyId, - 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. - * - * Note: This method will hard-revoke the subkey, meaning it cannot be re-bound at a later point. - * If you intend to re-bind the subkey in order to make it usable again at a later point in time, - * consider using {@link #revokeSubKey(long, SecretKeyRingProtector, RevocationAttributes)} - * and provide a soft revocation reason. - * - * @param subKeyId id of the subkey - * @param secretKeyRingProtector protector to unlock the secret key ring - * @return the builder - * - * @throws PGPException in case we cannot generate a revocation signature for the subkey - */ - default SecretKeyRingEditorInterface revokeSubKey( - long subKeyId, - @Nonnull SecretKeyRingProtector secretKeyRingProtector) - throws PGPException { - - return revokeSubKey( - subKeyId, - secretKeyRingProtector, - (RevocationSignatureSubpackets.Callback) 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. - * - * The provided subpackets callback is used to modify the revocation signatures subpackets. - * - * @param keyID id of the subkey - * @param secretKeyRingProtector protector to unlock the secret key ring - * @param subpacketsCallback 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 - */ - SecretKeyRingEditorInterface revokeSubKey( - long keyID, - @Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationSignatureSubpackets.Callback subpacketsCallback) - throws PGPException; - - /** - * Revoke the given userID. - * The revocation will be a hard revocation, rendering the user-id invalid for any past or future signatures. - * If you intend to re-certify the user-id at a later point in time, consider using - * {@link #revokeUserId(CharSequence, SecretKeyRingProtector, RevocationAttributes)} instead and provide - * a soft revocation reason. - * - * @param userId userId to revoke - * @param secretKeyRingProtector protector to unlock the primary key - * @return the builder - * - * @throws PGPException in case we cannot generate a revocation signature for the user-id - */ - default SecretKeyRingEditorInterface revokeUserId( - @Nonnull CharSequence userId, - @Nonnull SecretKeyRingProtector secretKeyRingProtector) - throws PGPException { - return revokeUserId(userId, secretKeyRingProtector, (RevocationAttributes) 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 - * - * @throws PGPException in case we cannot generate a revocation signature for the user-id - */ - SecretKeyRingEditorInterface revokeUserId( - @Nonnull CharSequence userId, - @Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationAttributes revocationAttributes) - throws PGPException; - - /** - * Revoke the provided user-id. - * Note: If you don't provide a {@link RevocationSignatureSubpackets.Callback} which - * sets a revocation reason ({@link RevocationAttributes}), the revocation might 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 secretKeyRingProtector protector to unlock the primary secret key - * @param subpacketCallback callback to modify the revocations subpackets - * @return builder - * - * @throws PGPException in case we cannot generate a revocation signature for the user-id - */ - SecretKeyRingEditorInterface revokeUserId( - @Nonnull CharSequence userId, - @Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationSignatureSubpackets.Callback subpacketCallback) - throws PGPException; - - /** - * Revoke all user-ids that match the provided {@link SelectUserId} filter. - * The provided {@link 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 {@link RevocationAttributes.Reason} for more information. - * - * @param userIdSelector user-id selector - * @param secretKeyRingProtector 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 - */ - SecretKeyRingEditorInterface revokeUserIds( - @Nonnull SelectUserId userIdSelector, - @Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationAttributes revocationAttributes) - throws PGPException; - - /** - * Revoke all user-ids that match the provided {@link SelectUserId} filter. - * The provided {@link 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 {@link RevocationAttributes.Reason} for more information. - * - * @param userIdSelector user-id selector - * @param secretKeyRingProtector protector to unlock the primary secret key - * @param subpacketsCallback callback to modify the revocations subpackets - * @return builder - * - * @throws PGPException in case we cannot generate a revocation signature for the user-id - */ - SecretKeyRingEditorInterface revokeUserIds( - @Nonnull SelectUserId userIdSelector, - @Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationSignatureSubpackets.Callback subpacketsCallback) - 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 - * - * @throws PGPException in case we cannot generate a new self-signature with the changed expiration date - */ - SecretKeyRingEditorInterface setExpirationDate( - @Nullable Date expiration, - @Nonnull SecretKeyRingProtector secretKeyRingProtector) - throws PGPException; - - /** - * Create a detached revocation certificate, which can be used to revoke the whole key. - * - * @param secretKeyRingProtector 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 - */ - PGPSignature createRevocationCertificate( - @Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationAttributes revocationAttributes) - throws PGPException; - - /** - * Create a detached revocation certificate, which can be used to revoke the specified subkey. - * - * @param subkeyId id of the subkey to be revoked - * @param secretKeyRingProtector 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 - */ - PGPSignature createRevocationCertificate( - long subkeyId, - @Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationAttributes revocationAttributes) - throws PGPException; - - /** - * Create a detached revocation certificate, which can be used to revoke the specified subkey. - * - * @param subkeyId id of the subkey to be revoked - * @param secretKeyRingProtector protector to unlock the primary key. - * @param certificateSubpacketsCallback callback to modify the subpackets of the revocation certificate. - * @return revocation certificate - * - * @throws PGPException in case we cannot generate a revocation certificate - */ - PGPSignature createRevocationCertificate( - long subkeyId, - @Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationSignatureSubpackets.Callback certificateSubpacketsCallback) - throws PGPException; - - /** - * Create a detached revocation certificate, which can be used to revoke the specified subkey. - * - * @param subkeyFingerprint fingerprint of the subkey to be revoked - * @param secretKeyRingProtector 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 - */ - default PGPSignature createRevocationCertificate( - OpenPgpFingerprint subkeyFingerprint, - SecretKeyRingProtector secretKeyRingProtector, - @Nullable 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 - * - * @throws PGPException in case the passphrase cannot be changed - */ - SecretKeyRingEditorInterface toNewPassphrase(Passphrase passphrase) - throws PGPException; - - /** - * Leave the key unprotected. - * - * @return editor builder - * - * @throws PGPException in case the passphrase cannot be changed - */ - 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 50cacf05..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/parsing/KeyRingReader.java +++ /dev/null @@ -1,377 +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.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.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 int MAX_ITERATIONS = 10000; - - @SuppressWarnings("CharsetObjectCanBeUsed") - public static final Charset UTF8 = Charset.forName("UTF-8"); - - /** - * Read a {@link PGPKeyRing} (either {@link PGPSecretKeyRing} or {@link PGPPublicKeyRing}) from the given - * {@link InputStream}. - * - * @param inputStream inputStream containing the OpenPGP key or certificate - * @return key ring - * @throws IOException in case of an IO error - */ - public PGPKeyRing keyRing(@Nonnull InputStream inputStream) - throws IOException { - return readKeyRing(inputStream); - } - - /** - * Read a {@link PGPKeyRing} (either {@link PGPSecretKeyRing} or {@link 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 - */ - public PGPKeyRing keyRing(@Nonnull byte[] bytes) - throws IOException { - return keyRing(new ByteArrayInputStream(bytes)); - } - - /** - * Read a {@link PGPKeyRing} (either {@link PGPSecretKeyRing} or {@link 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 - */ - public PGPKeyRing keyRing(@Nonnull String asciiArmored) - throws IOException { - return keyRing(asciiArmored.getBytes(UTF8)); - } - - 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); - } - - /** - * Read a {@link PGPKeyRing} (either {@link PGPSecretKeyRing} or {@link PGPPublicKeyRing}) from the given - * {@link InputStream}. - * This method will attempt to read at most {@link #MAX_ITERATIONS} objects from the stream before aborting. - * The first {@link PGPPublicKeyRing} or {@link PGPSecretKeyRing} will be returned. - * - * @param inputStream inputStream containing the OpenPGP key or certificate - * @return key ring - * @throws IOException in case of an IO error - */ - public static PGPKeyRing readKeyRing(@Nonnull InputStream inputStream) - throws IOException { - return readKeyRing(inputStream, MAX_ITERATIONS); - } - - /** - * Read a {@link PGPKeyRing} (either {@link PGPSecretKeyRing} or {@link PGPPublicKeyRing}) from the given - * {@link InputStream}. - * This method will attempt to read at most
maxIterations
objects from the stream before aborting. - * The first {@link PGPPublicKeyRing} or {@link 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 - */ - public static PGPKeyRing readKeyRing(@Nonnull InputStream inputStream, int maxIterations) - throws IOException { - PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory( - ArmorUtils.getDecoderStream(inputStream)); - int i = 0; - Object next; - do { - next = objectFactory.nextObject(); - if (next == null) { - return null; - } - if (next instanceof PGPMarker) { - continue; - } - if (next instanceof PGPSecretKeyRing) { - return (PGPSecretKeyRing) next; - } - if (next instanceof PGPPublicKeyRing) { - return (PGPPublicKeyRing) next; - } - } while (++i < maxIterations); - - throw new IOException("Loop exceeded max iteration count."); - } - - public static PGPPublicKeyRing readPublicKeyRing(@Nonnull InputStream inputStream) - throws IOException { - return readPublicKeyRing(inputStream, MAX_ITERATIONS); - } - - /** - * Read a public key ring from the provided {@link InputStream}. - * If more than maxIterations PGP packets are encountered before a {@link PGPPublicKeyRing} is read, - * an {@link 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 - */ - public static PGPPublicKeyRing readPublicKeyRing(@Nonnull InputStream inputStream, int maxIterations) - throws IOException { - PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory( - ArmorUtils.getDecoderStream(inputStream)); - int i = 0; - Object next; - do { - next = objectFactory.nextObject(); - if (next == null) { - return null; - } - if (next instanceof PGPMarker) { - continue; - } - if (next instanceof PGPPublicKeyRing) { - return (PGPPublicKeyRing) next; - } - } while (++i < maxIterations); - - throw new IOException("Loop exceeded max iteration count."); - } - - public static PGPPublicKeyRingCollection readPublicKeyRingCollection(@Nonnull InputStream inputStream) - throws IOException { - return readPublicKeyRingCollection(inputStream, MAX_ITERATIONS); - } - - /** - * Read a public key ring collection from the provided {@link InputStream}. - * If more than maxIterations PGP packets are encountered before the stream is exhausted, - * an {@link IOException} is thrown. - * - * @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 - */ - public static PGPPublicKeyRingCollection readPublicKeyRingCollection(@Nonnull InputStream inputStream, int maxIterations) - throws IOException { - PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory( - ArmorUtils.getDecoderStream(inputStream)); - - List rings = new ArrayList<>(); - int i = 0; - 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 (++i < maxIterations); - - throw new IOException("Loop exceeded max iteration count."); - } - - public static PGPSecretKeyRing readSecretKeyRing(@Nonnull InputStream inputStream) - throws IOException { - return readSecretKeyRing(inputStream, MAX_ITERATIONS); - } - - /** - * Read a secret key ring from the provided {@link InputStream}. - * If more than maxIterations PGP packets are encountered before a {@link PGPSecretKeyRing} is read, - * an {@link 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 - */ - public static PGPSecretKeyRing readSecretKeyRing(@Nonnull InputStream inputStream, int maxIterations) - throws IOException { - InputStream decoderStream = ArmorUtils.getDecoderStream(inputStream); - PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(decoderStream); - int i = 0; - 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 (++i < maxIterations); - - throw new IOException("Loop exceeded max iteration count."); - } - - public static PGPSecretKeyRingCollection readSecretKeyRingCollection(@Nonnull InputStream inputStream) - throws IOException { - return readSecretKeyRingCollection(inputStream, MAX_ITERATIONS); - } - - /** - * Read a secret key ring collection from the provided {@link InputStream}. - * If more than maxIterations PGP packets are encountered before the stream is exhausted, - * an {@link 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 - */ - public static PGPSecretKeyRingCollection readSecretKeyRingCollection(@Nonnull InputStream inputStream, - int maxIterations) - throws IOException { - PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory( - ArmorUtils.getDecoderStream(inputStream)); - - List rings = new ArrayList<>(); - int i = 0; - 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 (++i < maxIterations); - - throw new IOException("Loop exceeded max iteration count."); - } - - 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/BaseSecretKeyRingProtector.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/BaseSecretKeyRingProtector.java deleted file mode 100644 index 1a31d0e8..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/BaseSecretKeyRingProtector.java +++ /dev/null @@ -1,70 +0,0 @@ -// 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.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; - -import javax.annotation.Nullable; - -/** - * Basic {@link SecretKeyRingProtector} implementation that respects the users {@link KeyRingProtectionSettings} when - * encrypting keys. - */ -public class BaseSecretKeyRingProtector implements SecretKeyRingProtector { - - private final SecretKeyPassphraseProvider passphraseProvider; - private final KeyRingProtectionSettings protectionSettings; - - /** - * Constructor that uses the given {@link SecretKeyPassphraseProvider} to retrieve passphrases and PGPainless' - * default {@link KeyRingProtectionSettings}. - * - * @param passphraseProvider provider for passphrases - */ - public BaseSecretKeyRingProtector(SecretKeyPassphraseProvider passphraseProvider) { - this(passphraseProvider, KeyRingProtectionSettings.secureDefaultSettings()); - } - - /** - * Constructor that uses the given {@link SecretKeyPassphraseProvider} and {@link KeyRingProtectionSettings}. - * - * @param passphraseProvider provider for passphrases - * @param protectionSettings protection settings - */ - public BaseSecretKeyRingProtector(SecretKeyPassphraseProvider passphraseProvider, KeyRingProtectionSettings protectionSettings) { - this.passphraseProvider = passphraseProvider; - this.protectionSettings = protectionSettings; - } - - @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 || passphrase.isEmpty() ? null : - ImplementationFactory.getInstance().getPBESecretKeyDecryptor(passphrase); - } - - @Override - @Nullable - public PBESecretKeyEncryptor getEncryptor(Long keyId) throws PGPException { - Passphrase passphrase = passphraseProvider.getPassphraseFor(keyId); - return passphrase == null || passphrase.isEmpty() ? null : - ImplementationFactory.getInstance().getPBESecretKeyEncryptor( - protectionSettings.getEncryptionAlgorithm(), - protectionSettings.getHashAlgorithm(), - protectionSettings.getS2kCount(), - passphrase); - } -} 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 8cdb3efb..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/CachingSecretKeyRingProtector.java +++ /dev/null @@ -1,222 +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.OpenPgpFingerprint; -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. - * - * If you need to unlock multiple {@link PGPKeyRing PGPKeyRings}, it is advised to use a separate - * {@link CachingSecretKeyRingProtector} instance for each ring. - */ -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. - * If the cache already contains a passphrase for the given key-id, a {@link 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 - * {@link #replacePassphrase(Long, Passphrase)} to replace the passphrase. - * - * @param keyId id of the key - * @param passphrase passphrase - */ - public void addPassphrase(@Nonnull Long keyId, @Nonnull Passphrase passphrase) { - if (this.cache.containsKey(keyId)) { - throw new IllegalArgumentException("The cache already holds a passphrase for ID " + Long.toHexString(keyId) + ".\n" + - "If you want to replace the passphrase, use replacePassphrase(Long, Passphrase) instead."); - } - this.cache.put(keyId, passphrase); - } - - /** - * Replace the passphrase for the given key-id in the cache. - * - * @param keyId keyId - * @param passphrase passphrase - */ - public void replacePassphrase(@Nonnull Long keyId, @Nonnull Passphrase passphrase) { - this.cache.put(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 - * {@link 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 {@link #replacePassphrase(PGPKeyRing, Passphrase)} instead. - * - * If you need to unlock multiple {@link PGPKeyRing PGPKeyRings}, it is advised to use a separate - * {@link CachingSecretKeyRingProtector} instance for each ring. - * - * @param keyRing key ring - * @param passphrase passphrase - */ - public void addPassphrase(@Nonnull PGPKeyRing keyRing, @Nonnull Passphrase passphrase) { - Iterator keys = keyRing.getPublicKeys(); - // check for existing passphrases before doing anything - while (keys.hasNext()) { - long keyId = keys.next().getKeyID(); - if (cache.containsKey(keyId)) { - throw new IllegalArgumentException("The cache already holds a passphrase for ID " + Long.toHexString(keyId) + ".\n" + - "If you want to replace the passphrase, use replacePassphrase(PGPKeyRing, Passphrase) instead."); - } - } - - // only then insert - keys = keyRing.getPublicKeys(); - while (keys.hasNext()) { - PGPPublicKey publicKey = keys.next(); - addPassphrase(publicKey, passphrase); - } - } - - /** - * Replace the cached passphrases for all keys in the key ring with the provided passphrase. - * - * @param keyRing key ring - * @param passphrase passphrase - */ - public void replacePassphrase(@Nonnull PGPKeyRing keyRing, @Nonnull Passphrase passphrase) { - Iterator keys = keyRing.getPublicKeys(); - while (keys.hasNext()) { - PGPPublicKey publicKey = keys.next(); - replacePassphrase(publicKey.getKeyID(), passphrase); - } - } - - /** - * Remember the given passphrase for the given (sub-)key. - * - * @param key key - * @param passphrase passphrase - */ - public void addPassphrase(@Nonnull PGPPublicKey key, @Nonnull Passphrase passphrase) { - addPassphrase(key.getKeyID(), passphrase); - } - - public void addPassphrase(@Nonnull OpenPgpFingerprint fingerprint, @Nonnull 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.remove(keyId); - if (passphrase != null) { - passphrase.clear(); - } - } - - /** - * 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) { - Passphrase passphrase = cache.get(keyId); - return passphrase != null && passphrase.isValid(); - } - - @Override - public boolean hasPassphraseFor(Long keyId) { - return hasPassphrase(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 a93534ab..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/KeyRingProtectionSettings.java +++ /dev/null @@ -1,99 +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; - -/** - * Secret key protection settings for iterated and salted S2K. - */ -public class KeyRingProtectionSettings { - - private final SymmetricKeyAlgorithm encryptionAlgorithm; - private final HashAlgorithm hashAlgorithm; - private final int s2kCount; - - /** - * Create a {@link KeyRingProtectionSettings} object using the given encryption algorithm, SHA1 and - * 65536 iterations. - * - * @param encryptionAlgorithm encryption algorithm - */ - public KeyRingProtectionSettings(@Nonnull SymmetricKeyAlgorithm encryptionAlgorithm) { - this(encryptionAlgorithm, HashAlgorithm.SHA1, 0x60); // Same s2kCount (encoded) as used in BC. - } - - /** - * Constructor for custom salted and iterated S2K protection settings. - * The salt gets randomly chosen by the library each time. - * - * Note, that the s2kCount is the already encoded single-octet number. - * - * @see Encoding Formula - * - * @param encryptionAlgorithm encryption algorithm - * @param hashAlgorithm hash algorithm - * @param s2kCount encoded s2k iteration count - */ - public KeyRingProtectionSettings(@Nonnull SymmetricKeyAlgorithm encryptionAlgorithm, @Nonnull HashAlgorithm hashAlgorithm, int s2kCount) { - this.encryptionAlgorithm = validateEncryptionAlgorithm(encryptionAlgorithm); - this.hashAlgorithm = hashAlgorithm; - if (s2kCount < 1) { - throw new IllegalArgumentException("s2kCount cannot be less than 1."); - } - this.s2kCount = s2kCount; - } - - private static SymmetricKeyAlgorithm validateEncryptionAlgorithm(SymmetricKeyAlgorithm encryptionAlgorithm) { - switch (encryptionAlgorithm) { - case NULL: - throw new IllegalArgumentException("Unencrypted is not allowed here!"); - default: - return encryptionAlgorithm; - } - } - - /** - * Secure default settings using {@link SymmetricKeyAlgorithm#AES_256}, {@link HashAlgorithm#SHA256} - * and an iteration count of 65536. - * - * @return secure protection settings - */ - public static KeyRingProtectionSettings secureDefaultSettings() { - return new KeyRingProtectionSettings(SymmetricKeyAlgorithm.AES_256, HashAlgorithm.SHA256, 0x60); - } - - /** - * Return the encryption algorithm. - * - * @return encryption algorithm - */ - public @Nonnull SymmetricKeyAlgorithm getEncryptionAlgorithm() { - return encryptionAlgorithm; - } - - /** - * Return the hash algorithm. - * - * @return hash algorithm - */ - public @Nonnull HashAlgorithm getHashAlgorithm() { - return hashAlgorithm; - } - - /** - * Return the (encoded!) s2k iteration count. - * - * @see Encoding Formula - * - * @return encoded s2k count - */ - 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 c5745068..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/PasswordBasedSecretKeyRingProtector.java +++ /dev/null @@ -1,78 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.protection; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import org.bouncycastle.openpgp.PGPKeyRing; -import org.bouncycastle.openpgp.PGPSecretKey; -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; - -/** - * 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 extends BaseSecretKeyRingProtector { - - public PasswordBasedSecretKeyRingProtector(@Nonnull SecretKeyPassphraseProvider passphraseProvider) { - super(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) { - super(passphraseProvider, settings); - } - - public static PasswordBasedSecretKeyRingProtector forKey(PGPKeyRing keyRing, Passphrase passphrase) { - SecretKeyPassphraseProvider passphraseProvider = new SecretKeyPassphraseProvider() { - @Override - @Nullable - public Passphrase getPassphraseFor(Long keyId) { - return hasPassphrase(keyId) ? passphrase : null; - } - - @Override - public boolean hasPassphrase(Long keyId) { - return keyRing.getPublicKey(keyId) != null; - } - }; - return new PasswordBasedSecretKeyRingProtector(passphraseProvider); - } - - public static PasswordBasedSecretKeyRingProtector forKey(PGPSecretKey key, Passphrase passphrase) { - return forKeyId(key.getPublicKey().getKeyID(), passphrase); - } - - public static PasswordBasedSecretKeyRingProtector forKeyId(long singleKeyId, Passphrase passphrase) { - SecretKeyPassphraseProvider passphraseProvider = new SecretKeyPassphraseProvider() { - @Nullable - @Override - public Passphrase getPassphraseFor(Long keyId) { - if (keyId == singleKeyId) { - return passphrase; - } - return null; - } - - @Override - public boolean hasPassphrase(Long keyId) { - return keyId == singleKeyId; - } - }; - return new PasswordBasedSecretKeyRingProtector(passphraseProvider); - } - -} 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 d7ed5c85..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/SecretKeyRingProtector.java +++ /dev/null @@ -1,168 +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.Nonnull; -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.key.protection.passphrase_provider.SolitaryPassphraseProvider; -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 { - - /** - * 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 - */ - 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 - * - * @throws PGPException if the decryptor cannot be created for some reason - */ - @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 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 - * @deprecated use {@link #unlockEachKeyWith(Passphrase, PGPSecretKeyRing)} instead. - * - * TODO: Remove in 1.2.X - */ - @Deprecated - static SecretKeyRingProtector unlockAllKeysWith(@Nonnull Passphrase passphrase, @Nonnull PGPSecretKeyRing keys) { - return unlockEachKeyWith(passphrase, keys); - } - - /** - * 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 - */ - static SecretKeyRingProtector unlockEachKeyWith(@Nonnull Passphrase passphrase, @Nonnull PGPSecretKeyRing keys) { - Map map = new ConcurrentHashMap<>(); - for (PGPSecretKey secretKey : keys) { - map.put(secretKey.getKeyID(), passphrase); - } - return fromPassphraseMap(map); - } - - /** - * Use the provided passphrase to unlock any key. - * - * @param passphrase passphrase - * @return protector - */ - static SecretKeyRingProtector unlockAnyKeyWith(@Nonnull Passphrase passphrase) { - return new BaseSecretKeyRingProtector(new 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 - * {@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(@Nonnull Passphrase passphrase, @Nonnull PGPSecretKey key) { - return PasswordBasedSecretKeyRingProtector.forKey(key, passphrase); - } - - static SecretKeyRingProtector unlockSingleKeyWith(@Nonnull Passphrase passphrase, long keyId) { - return PasswordBasedSecretKeyRingProtector.forKeyId(keyId, 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 - * (e.g. 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(@Nonnull 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 78c849ab..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/UnlockSecretKey.java +++ /dev/null @@ -1,66 +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.PGPainless; -import org.pgpainless.exception.KeyIntegrityException; -import org.pgpainless.exception.WrongPassphraseException; -import org.pgpainless.key.info.KeyInfo; -import org.pgpainless.key.util.PublicKeyParameterValidationUtil; -import org.pgpainless.util.Passphrase; - -public final class UnlockSecretKey { - - private UnlockSecretKey() { - - } - - public static PGPPrivateKey unlockSecretKey(PGPSecretKey secretKey, SecretKeyRingProtector protector) - throws PGPException, KeyIntegrityException { - - PBESecretKeyDecryptor decryptor = null; - if (KeyInfo.isEncrypted(secretKey)) { - decryptor = protector.getDecryptor(secretKey.getKeyID()); - } - PGPPrivateKey privateKey = unlockSecretKey(secretKey, decryptor); - return privateKey; - } - - public static PGPPrivateKey unlockSecretKey(PGPSecretKey secretKey, PBESecretKeyDecryptor decryptor) - throws PGPException { - PGPPrivateKey privateKey; - try { - privateKey = secretKey.extractPrivateKey(decryptor); - } catch (PGPException e) { - throw new WrongPassphraseException(secretKey.getKeyID(), e); - } - - if (privateKey == null) { - int s2kType = secretKey.getS2K().getType(); - if (s2kType >= 100 && s2kType <= 110) { - throw new PGPException("Cannot decrypt secret key" + Long.toHexString(secretKey.getKeyID()) + ": " + - "Unsupported private S2K usage type " + s2kType); - } - - throw new PGPException("Cannot decrypt secret key."); - } - - if (PGPainless.getPolicy().isEnableKeyParameterValidation()) { - PublicKeyParameterValidationUtil.verifyPublicKeyParameterIntegrity(privateKey, secretKey.getPublicKey()); - } - - return privateKey; - } - - public static PGPPrivateKey unlockSecretKey(PGPSecretKey secretKey, Passphrase passphrase) - throws PGPException, KeyIntegrityException { - 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 7ebac8fd..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyIdUtil.java +++ /dev/null @@ -1,37 +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(); - } - - public static String formatKeyId(long keyId) { - return Long.toHexString(keyId).toUpperCase(); - } -} 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 2ce058fd..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java +++ /dev/null @@ -1,512 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.util; - -import java.io.ByteArrayOutputStream; -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 javax.annotation.Nullable; - -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.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.PGPUserAttributeSubpacketVector; -import org.pgpainless.PGPainless; -import org.pgpainless.implementation.ImplementationFactory; -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 - */ - @Nonnull - public static PGPSecretKey requirePrimarySecretKeyFrom(@Nonnull 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 - */ - @Nullable - public static PGPSecretKey getPrimarySecretKeyFrom(@Nonnull 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 - */ - @Nonnull - public static PGPPublicKey requirePrimaryPublicKeyFrom(@Nonnull 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 - */ - @Nullable - public static PGPPublicKey getPrimaryPublicKeyFrom(@Nonnull PGPKeyRing keyRing) { - PGPPublicKey primaryPublicKey = keyRing.getPublicKey(); - if (primaryPublicKey.isMasterKey()) { - return primaryPublicKey; - } - return null; - } - - /** - * Return the public key with the given subKeyId from the keyRing. - * If no such subkey exists, return null. - * @param keyRing key ring - * @param subKeyId subkey id - * @return subkey or null - */ - @Nullable - public static PGPPublicKey getPublicKeyFrom(@Nonnull PGPKeyRing keyRing, long subKeyId) { - return keyRing.getPublicKey(subKeyId); - } - - /** - * Require the public key with the given subKeyId from the keyRing. - * If no such subkey exists, throw an {@link NoSuchElementException}. - * - * @param keyRing key ring - * @param subKeyId subkey id - * @return subkey - */ - @Nonnull - public static PGPPublicKey requirePublicKeyFrom(@Nonnull PGPKeyRing keyRing, long subKeyId) { - PGPPublicKey publicKey = getPublicKeyFrom(keyRing, subKeyId); - if (publicKey == null) { - throw new NoSuchElementException("KeyRing does not contain public key with keyID " + Long.toHexString(subKeyId)); - } - return publicKey; - } - - /** - * Require the secret key with the given secret subKeyId from the secret keyRing. - * If no such subkey exists, throw an {@link NoSuchElementException}. - * - * @param keyRing secret key ring - * @param subKeyId subkey id - * @return secret subkey - */ - @Nonnull - public static PGPSecretKey requireSecretKeyFrom(@Nonnull PGPSecretKeyRing keyRing, long subKeyId) { - PGPSecretKey secretKey = keyRing.getSecretKey(subKeyId); - if (secretKey == null) { - throw new NoSuchElementException("KeyRing does not contain secret key with keyID " + Long.toHexString(subKeyId)); - } - return secretKey; - } - - @Nonnull - public static PGPPublicKeyRing publicKeys(@Nonnull PGPKeyRing keys) { - if (keys instanceof PGPPublicKeyRing) { - return (PGPPublicKeyRing) keys; - } else if (keys instanceof PGPSecretKeyRing) { - return publicKeyRingFrom((PGPSecretKeyRing) keys); - } else { - throw new IllegalArgumentException("Unknown keys class: " + keys.getClass().getName()); - } - } - - /** - * Extract a {@link PGPPublicKeyRing} containing all public keys from the provided {@link PGPSecretKeyRing}. - * - * @param secretKeys secret key ring - * @return public key ring - */ - @Nonnull - public static PGPPublicKeyRing publicKeyRingFrom(@Nonnull PGPSecretKeyRing secretKeys) { - List publicKeyList = new ArrayList<>(); - Iterator publicKeyIterator = secretKeys.getPublicKeys(); - while (publicKeyIterator.hasNext()) { - publicKeyList.add(publicKeyIterator.next()); - } - PGPPublicKeyRing publicKeyRing = new PGPPublicKeyRing(publicKeyList); - return publicKeyRing; - } - - /** - * Extract {@link PGPPublicKeyRing PGPPublicKeyRings} from all {@link PGPSecretKeyRing PGPSecretKeyRings} in - * the given {@link PGPSecretKeyRingCollection} and return them as a {@link PGPPublicKeyRingCollection}. - * - * @param secretKeyRings secret key ring collection - * @return public key ring collection - */ - @Nonnull - public static PGPPublicKeyRingCollection publicKeyRingCollectionFrom(@Nonnull PGPSecretKeyRingCollection secretKeyRings) { - List certificates = new ArrayList<>(); - for (PGPSecretKeyRing secretKey : secretKeyRings) { - certificates.add(PGPainless.extractCertificate(secretKey)); - } - return new PGPPublicKeyRingCollection(certificates); - } - - /** - * 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 (e.g. wrong passphrase) - */ - @Nonnull - public static PGPPrivateKey unlockSecretKey(@Nonnull PGPSecretKey secretKey, @Nonnull SecretKeyRingProtector protector) - throws PGPException { - return UnlockSecretKey.unlockSecretKey(secretKey, protector); - } - - /** - * Create a new {@link PGPPublicKeyRingCollection} from an array of {@link PGPPublicKeyRing PGPPublicKeyRings}. - * - * @param rings array of public key rings - * @return key ring collection - */ - @Nonnull - public static PGPPublicKeyRingCollection keyRingsToKeyRingCollection(@Nonnull PGPPublicKeyRing... rings) { - return new PGPPublicKeyRingCollection(Arrays.asList(rings)); - } - - /** - * Create a new {@link PGPSecretKeyRingCollection} from an array of {@link PGPSecretKeyRing PGPSecretKeyRings}. - * - * @param rings array of secret key rings - * @return secret key ring collection - */ - @Nonnull - public static PGPSecretKeyRingCollection keyRingsToKeyRingCollection(@Nonnull PGPSecretKeyRing... rings) { - return new PGPSecretKeyRingCollection(Arrays.asList(rings)); - } - - /** - * Return true, if the given {@link PGPPublicKeyRing} contains a {@link PGPPublicKey} for the given key id. - * - * @param ring public key ring - * @param keyId id of the key in question - * @return true if ring contains said key, false otherwise - */ - public static boolean keyRingContainsKeyWithId(@Nonnull PGPPublicKeyRing ring, - long keyId) { - return ring.getPublicKey(keyId) != null; - } - - /** - * Inject a key certification for the primary key into the given key ring. - * - * @param keyRing key ring - * @param certification key signature - * @return key ring with injected signature - * @param either {@link PGPPublicKeyRing} or {@link PGPSecretKeyRing} - */ - @Nonnull - public static T injectCertification(@Nonnull T keyRing, - @Nonnull PGPSignature certification) { - return injectCertification(keyRing, keyRing.getPublicKey(), 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 {@link PGPPublicKeyRing} or {@link PGPSecretKeyRing} - * @return key ring with injected signature - * - * @throws NoSuchElementException in case that the signed key is not part of the key ring - */ - @Nonnull - public static T injectCertification(@Nonnull T keyRing, - @Nonnull PGPPublicKey certifiedKey, - @Nonnull PGPSignature certification) { - PGPSecretKeyRing secretKeys = null; - PGPPublicKeyRing publicKeys; - if (keyRing instanceof PGPSecretKeyRing) { - secretKeys = (PGPSecretKeyRing) keyRing; - publicKeys = PGPainless.extractCertificate(secretKeys); - } else { - publicKeys = (PGPPublicKeyRing) keyRing; - } - - certifiedKey = PGPPublicKey.addCertification(certifiedKey, certification); - List publicKeyList = new ArrayList<>(); - Iterator publicKeyIterator = publicKeys.iterator(); - boolean added = false; - while (publicKeyIterator.hasNext()) { - PGPPublicKey key = publicKeyIterator.next(); - if (key.getKeyID() == certifiedKey.getKeyID()) { - added = true; - publicKeyList.add(certifiedKey); - } else { - publicKeyList.add(key); - } - } - if (!added) { - throw new NoSuchElementException("Cannot find public key with id " + Long.toHexString(certifiedKey.getKeyID()) + " in the provided key ring."); - } - - publicKeys = new PGPPublicKeyRing(publicKeyList); - if (secretKeys == null) { - return (T) publicKeys; - } else { - secretKeys = PGPSecretKeyRing.replacePublicKeys(secretKeys, publicKeys); - return (T) secretKeys; - } - } - - /** - * Inject a user-id certification into the given key ring. - * - * @param keyRing key ring - * @param userId signed user-id - * @param certification signature - * @param either {@link PGPPublicKeyRing} or {@link PGPSecretKeyRing} - * @return key ring with injected certification - */ - @Nonnull - public static T injectCertification(@Nonnull T keyRing, - @Nonnull String userId, - @Nonnull PGPSignature certification) { - PGPSecretKeyRing secretKeys = null; - PGPPublicKeyRing publicKeys; - if (keyRing instanceof PGPSecretKeyRing) { - secretKeys = (PGPSecretKeyRing) keyRing; - publicKeys = PGPainless.extractCertificate(secretKeys); - } else { - publicKeys = (PGPPublicKeyRing) keyRing; - } - - Iterator publicKeyIterator = publicKeys.iterator(); - PGPPublicKey primaryKey = publicKeyIterator.next(); - primaryKey = PGPPublicKey.addCertification(primaryKey, userId, certification); - - List publicKeyList = new ArrayList<>(); - publicKeyList.add(primaryKey); - while (publicKeyIterator.hasNext()) { - publicKeyList.add(publicKeyIterator.next()); - } - - publicKeys = new PGPPublicKeyRing(publicKeyList); - if (secretKeys == null) { - return (T) publicKeys; - } else { - secretKeys = PGPSecretKeyRing.replacePublicKeys(secretKeys, publicKeys); - return (T) secretKeys; - } - } - - /** - * 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 {@link PGPPublicKeyRing} or {@link PGPSecretKeyRing} - * @return key ring with injected user-attribute certification - */ - @Nonnull - public static T injectCertification(@Nonnull T keyRing, - @Nonnull PGPUserAttributeSubpacketVector userAttributes, - @Nonnull PGPSignature certification) { - PGPSecretKeyRing secretKeys = null; - PGPPublicKeyRing publicKeys; - if (keyRing instanceof PGPSecretKeyRing) { - secretKeys = (PGPSecretKeyRing) keyRing; - publicKeys = PGPainless.extractCertificate(secretKeys); - } else { - publicKeys = (PGPPublicKeyRing) keyRing; - } - - Iterator publicKeyIterator = publicKeys.iterator(); - PGPPublicKey primaryKey = publicKeyIterator.next(); - primaryKey = PGPPublicKey.addCertification(primaryKey, userAttributes, certification); - - List publicKeyList = new ArrayList<>(); - publicKeyList.add(primaryKey); - while (publicKeyIterator.hasNext()) { - publicKeyList.add(publicKeyIterator.next()); - } - - publicKeys = new PGPPublicKeyRing(publicKeyList); - if (secretKeys == null) { - return (T) publicKeys; - } else { - secretKeys = PGPSecretKeyRing.replacePublicKeys(secretKeys, publicKeys); - return (T) secretKeys; - } - } - - /** - * Inject a {@link PGPPublicKey} into the given key ring. - * - * @param keyRing key ring - * @param publicKey public key - * @param either {@link PGPPublicKeyRing} or {@link PGPSecretKeyRing} - * @return key ring with injected public key - */ - @Nonnull - public static T keysPlusPublicKey(@Nonnull T keyRing, - @Nonnull PGPPublicKey publicKey) { - PGPSecretKeyRing secretKeys = null; - PGPPublicKeyRing publicKeys; - if (keyRing instanceof PGPSecretKeyRing) { - secretKeys = (PGPSecretKeyRing) keyRing; - publicKeys = PGPainless.extractCertificate(secretKeys); - } else { - publicKeys = (PGPPublicKeyRing) keyRing; - } - - publicKeys = PGPPublicKeyRing.insertPublicKey(publicKeys, publicKey); - if (secretKeys == null) { - return (T) publicKeys; - } else { - secretKeys = PGPSecretKeyRing.insertOrReplacePublicKey(secretKeys, publicKey); - return (T) secretKeys; - } - } - - /** - * Inject a {@link PGPSecretKey} into a {@link PGPSecretKeyRing}. - * - * @param secretKeys secret key ring - * @param secretKey secret key - * @return secret key ring with injected secret key - */ - @Nonnull - public static PGPSecretKeyRing keysPlusSecretKey(@Nonnull PGPSecretKeyRing secretKeys, - @Nonnull PGPSecretKey secretKey) { - 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 - */ - @Nonnull - public static PGPSecretKey secretKeyPlusSignature(@Nonnull PGPSecretKey secretKey, - @Nonnull PGPSignature signature) { - PGPPublicKey publicKey = secretKey.getPublicKey(); - publicKey = PGPPublicKey.addCertification(publicKey, signature); - PGPSecretKey newSecretKey = PGPSecretKey.replacePublicKey(secretKey, publicKey); - return newSecretKey; - } - - /** - * 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 secretKeyId 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 - * - * @deprecated use {@link #stripSecretKey(PGPSecretKeyRing, long)} instead. - * TODO: Remove in 1.2.X - */ - @Nonnull - @Deprecated - public static PGPSecretKeyRing removeSecretKey(@Nonnull PGPSecretKeyRing secretKeys, - long secretKeyId) - throws IOException, PGPException { - return stripSecretKey(secretKeys, secretKeyId); - } - - /** - * 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 secretKeyId 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 - */ - @Nonnull - public static PGPSecretKeyRing stripSecretKey(@Nonnull PGPSecretKeyRing secretKeys, - long secretKeyId) - throws IOException, PGPException { - - if (secretKeys.getPublicKey().getKeyID() == secretKeyId) { - throw new IllegalArgumentException("Bouncy Castle currently cannot deal with stripped secret primary keys."); - } - - if (secretKeys.getSecretKey(secretKeyId) == null) { - throw new NoSuchElementException("PGPSecretKeyRing does not contain secret key " + Long.toHexString(secretKeyId)); - } - - // Since BCs constructors for secret key rings are mostly private, we need to encode the key ring how we want it - // and then parse it again. - ByteArrayOutputStream encoded = new ByteArrayOutputStream(); - for (PGPSecretKey secretKey : secretKeys) { - if (secretKey.getKeyID() == secretKeyId) { - // only encode the public part of the target key - secretKey.getPublicKey().encode(encoded); - } else { - // otherwise, encode secret + public key - secretKey.encode(encoded); - } - } - for (Iterator it = secretKeys.getExtraPublicKeys(); it.hasNext(); ) { - PGPPublicKey extra = it.next(); - extra.encode(encoded); - } - // Parse the key back into an object - return new PGPSecretKeyRing(encoded.toByteArray(), ImplementationFactory.getInstance().getKeyFingerprintCalculator()); - } -} 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 e97a2d7a..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 @@ -34,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(); @@ -71,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; } diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/PublicKeyParameterValidationUtil.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/PublicKeyParameterValidationUtil.java deleted file mode 100644 index 344f063b..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/PublicKeyParameterValidationUtil.java +++ /dev/null @@ -1,291 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.util; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.math.BigInteger; -import java.security.SecureRandom; - -import org.bouncycastle.bcpg.BCPGKey; -import org.bouncycastle.bcpg.DSAPublicBCPGKey; -import org.bouncycastle.bcpg.DSASecretBCPGKey; -import org.bouncycastle.bcpg.EdDSAPublicBCPGKey; -import org.bouncycastle.bcpg.EdSecretBCPGKey; -import org.bouncycastle.bcpg.ElGamalPublicBCPGKey; -import org.bouncycastle.bcpg.ElGamalSecretBCPGKey; -import org.bouncycastle.bcpg.RSAPublicBCPGKey; -import org.bouncycastle.bcpg.RSASecretBCPGKey; -import org.bouncycastle.openpgp.PGPEncryptedDataGenerator; -import org.bouncycastle.openpgp.PGPEncryptedDataList; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPrivateKey; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData; -import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.PGPSignatureGenerator; -import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; -import org.bouncycastle.util.Arrays; -import org.bouncycastle.util.io.Streams; -import org.pgpainless.algorithm.HashAlgorithm; -import org.pgpainless.algorithm.PublicKeyAlgorithm; -import org.pgpainless.algorithm.SignatureType; -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; -import org.pgpainless.exception.KeyIntegrityException; -import org.pgpainless.implementation.ImplementationFactory; - -/** - * 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 - */ -public class PublicKeyParameterValidationUtil { - - public static void verifyPublicKeyParameterIntegrity(PGPPrivateKey privateKey, PGPPublicKey publicKey) - throws KeyIntegrityException { - PublicKeyAlgorithm publicKeyAlgorithm = PublicKeyAlgorithm.requireFromId(publicKey.getAlgorithm()); - boolean valid = true; - - // Algorithm specific validations - BCPGKey key = privateKey.getPrivateKeyDataPacket(); - if (key instanceof RSASecretBCPGKey) { - valid = verifyRSAKeyIntegrity( - (RSASecretBCPGKey) key, - (RSAPublicBCPGKey) publicKey.getPublicKeyPacket().getKey()) - && valid; - } else if (key instanceof EdSecretBCPGKey) { - valid = verifyEdDsaKeyIntegrity( - (EdSecretBCPGKey) key, - (EdDSAPublicBCPGKey) publicKey.getPublicKeyPacket().getKey()) - && valid; - } else if (key instanceof DSASecretBCPGKey) { - valid = verifyDsaKeyIntegrity( - (DSASecretBCPGKey) key, - (DSAPublicBCPGKey) publicKey.getPublicKeyPacket().getKey()) - && valid; - } else if (key instanceof ElGamalSecretBCPGKey) { - valid = verifyElGamalKeyIntegrity( - (ElGamalSecretBCPGKey) key, - (ElGamalPublicBCPGKey) publicKey.getPublicKeyPacket().getKey()) - && valid; - } - - if (!valid) { - throw new 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 (publicKeyAlgorithm.isSigningCapable()) { - valid = verifyCanSign(privateKey, publicKey); - } - if (publicKeyAlgorithm.isEncryptionCapable()) { - valid = verifyCanDecrypt(privateKey, publicKey) && valid; - } - - if (!valid) { - throw new KeyIntegrityException(); - } - } - - /** - * Verify that the public key can be used to successfully verify a signature made by the private key. - * @param privateKey private key - * @param publicKey public key - * @return false if signature verification fails - */ - private static boolean verifyCanSign(PGPPrivateKey privateKey, PGPPublicKey publicKey) { - SecureRandom random = new SecureRandom(); - PublicKeyAlgorithm publicKeyAlgorithm = PublicKeyAlgorithm.requireFromId(publicKey.getAlgorithm()); - PGPSignatureGenerator signatureGenerator = new PGPSignatureGenerator( - ImplementationFactory.getInstance().getPGPContentSignerBuilder(publicKeyAlgorithm, HashAlgorithm.SHA256) - ); - - try { - signatureGenerator.init(SignatureType.TIMESTAMP.getCode(), privateKey); - - byte[] data = new byte[512]; - random.nextBytes(data); - - signatureGenerator.update(data); - PGPSignature sig = signatureGenerator.generate(); - - sig.init(ImplementationFactory.getInstance().getPGPContentVerifierBuilderProvider(), publicKey); - sig.update(data); - return sig.verify(); - } catch (PGPException e) { - return false; - } - } - - /** - * Verify that the public key can be used to encrypt a message which can successfully be - * decrypted using the private key. - * @param privateKey private key - * @param publicKey public key - * @return false if decryption of a message encrypted with the public key fails - */ - private static boolean verifyCanDecrypt(PGPPrivateKey privateKey, PGPPublicKey publicKey) { - SecureRandom random = new SecureRandom(); - PGPEncryptedDataGenerator encryptedDataGenerator = new PGPEncryptedDataGenerator( - ImplementationFactory.getInstance().getPGPDataEncryptorBuilder(SymmetricKeyAlgorithm.AES_256) - ); - encryptedDataGenerator.addMethod( - ImplementationFactory.getInstance().getPublicKeyKeyEncryptionMethodGenerator(publicKey)); - - byte[] data = new byte[1024]; - random.nextBytes(data); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - try { - OutputStream outputStream = encryptedDataGenerator.open(out, new byte[1024]); - outputStream.write(data); - encryptedDataGenerator.close(); - PGPEncryptedDataList encryptedDataList = new PGPEncryptedDataList(out.toByteArray()); - PublicKeyDataDecryptorFactory decryptorFactory = - ImplementationFactory.getInstance().getPublicKeyDataDecryptorFactory(privateKey); - PGPPublicKeyEncryptedData encryptedData = - (PGPPublicKeyEncryptedData) encryptedDataList.getEncryptedDataObjects().next(); - InputStream decrypted = encryptedData.getDataStream(decryptorFactory); - out = new ByteArrayOutputStream(); - Streams.pipeAll(decrypted, out); - decrypted.close(); - } catch (IOException | PGPException e) { - return false; - } - - return Arrays.constantTimeAreEqual(data, out.toByteArray()); - } - - private static boolean verifyEdDsaKeyIntegrity(EdSecretBCPGKey privateKey, EdDSAPublicBCPGKey publicKey) - throws KeyIntegrityException { - // TODO: Implement - return true; - } - - private static boolean verifyDsaKeyIntegrity(DSASecretBCPGKey privateKey, DSAPublicBCPGKey publicKey) - throws KeyIntegrityException { - // 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 - final int certainty = 40; - BigInteger pG = publicKey.getG(); - BigInteger pP = publicKey.getP(); - BigInteger pQ = publicKey.getQ(); - BigInteger pY = publicKey.getY(); - BigInteger sX = privateKey.getX(); - - boolean pPrime = pP.isProbablePrime(certainty); - if (!pPrime) { - return false; - } - - boolean qPrime = pQ.isProbablePrime(certainty); - if (!qPrime) { - return false; - } - - // q > 160 bits - boolean qLarge = pQ.bitLength() > 160; - if (!qLarge) { - return false; - } - - // q divides p - 1 - boolean qDividesPminus1 = pP.subtract(BigInteger.ONE).mod(pQ).equals(BigInteger.ZERO); - if (!qDividesPminus1) { - return false; - } - - // 1 < g < p - boolean gInBounds = BigInteger.ONE.max(pG).equals(pG) && pG.max(pP).equals(pP); - if (!gInBounds) { - return false; - } - - // g^q = 1 mod p - boolean gPowXModPEquals1 = pG.modPow(pQ, pP).equals(BigInteger.ONE); - if (!gPowXModPEquals1) { - return false; - } - - // y = g^x mod p - boolean yEqualsGPowXModP = pY.equals(pG.modPow(sX, pP)); - if (!yEqualsGPowXModP) { - return false; - } - - return true; - } - - private static boolean verifyRSAKeyIntegrity(RSASecretBCPGKey secretKey, RSAPublicBCPGKey publicKey) - throws KeyIntegrityException { - // Verify that the public keys N is equal to private keys p*q - return publicKey.getModulus().equals(secretKey.getPrimeP().multiply(secretKey.getPrimeQ())); - } - - /** - * Validate ElGamal public key parameters. - * - * Original implementation by the openpgpjs authors: - * , 2021 Flowcrypt a.s. -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.util; - -import java.util.Comparator; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -public final class UserId implements CharSequence { - - // 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 - private static final Pattern emailPattern = Pattern.compile("(?:[\\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])+)])"); - - // User-ID Regex - // "Firstname Lastname (Comment) " - // All groups are optional - // https://www.rfc-editor.org/rfc/rfc5322#page-16 - private static final Pattern nameAddrPattern = Pattern.compile("^((?.+?)\\s)?(\\((?.+?)\\)\\s)?(<(?.+?)>)?$"); - - 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(@Nonnull String name) { - this.name = name; - return this; - } - - public Builder withComment(@Nonnull String comment) { - this.comment = comment; - return this; - } - - public Builder withEmail(@Nonnull String 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); - } - } - - /** - * Parse a {@link 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, {@link #parse(String)} 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. - * - * @see
RFC5322 §3.4. Address Specification - * @param string user-id - * @return parsed {@link UserId} object - */ - public static UserId parse(@Nonnull String string) { - Builder builder = newBuilder(); - string = string.trim(); - Matcher matcher = nameAddrPattern.matcher(string); - if (matcher.find()) { - String name = matcher.group(2); - String comment = matcher.group(4); - String mail = matcher.group(6); - matcher = emailPattern.matcher(mail); - if (!matcher.matches()) { - throw new IllegalArgumentException("Malformed email address"); - } - - if (name != null) { - builder.withName(name); - } - if (comment != null) { - builder.withComment(comment); - } - builder.withEmail(mail); - } else { - matcher = emailPattern.matcher(string); - if (matcher.matches()) { - builder.withEmail(string); - } else { - throw new IllegalArgumentException("Malformed email address"); - } - } - return builder.build(); - } - - private final String name; - private final String comment; - private final String email; - private long hash = Long.MAX_VALUE; - - private UserId(@Nullable String name, @Nullable String comment, @Nullable String email) { - this.name = name == null ? null : name.trim(); - this.comment = comment == null ? null : comment.trim(); - this.email = email == null ? null : email.trim(); - } - - public static UserId onlyEmail(@Nonnull String email) { - return new UserId(null, null, email); - } - - public static UserId nameAndEmail(@Nonnull String name, @Nonnull String 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 getName(false); - } - - public String getName(boolean preserveQuotes) { - if (name == null || name.isEmpty()) { - return name; - } - - if (name.startsWith("\"")) { - if (preserveQuotes) { - return name; - } - String withoutQuotes = name.substring(1); - if (withoutQuotes.endsWith("\"")) { - withoutQuotes = withoutQuotes.substring(0, withoutQuotes.length() - 1); - } - return withoutQuotes; - } - 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 @Nonnull CharSequence subSequence(int i, int i1) { - return toString().subSequence(i, i1); - } - - @Override - public @Nonnull String toString() { - StringBuilder sb = new StringBuilder(); - if (name != null && !name.isEmpty()) { - sb.append(getName(true)); - } - if (comment != null && !comment.isEmpty()) { - if (sb.length() > 0) { - sb.append(' '); - } - sb.append('(').append(comment).append(')'); - } - if (email != null && !email.isEmpty()) { - if (sb.length() > 0) { - sb.append(' '); - } - sb.append('<').append(email).append('>'); - } - return sb.toString(); - } - - /** - * Returns a string representation of the object. - * @return a string representation of the object. - * @deprecated use {@link #toString()} instead. - */ - @Deprecated - public String asString() { - return 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))); - } - - public static int compare(@Nullable UserId o1, @Nullable UserId o2, @Nonnull Comparator comparator) { - return comparator.compare(o1, o2); - } - - public static class DefaultComparator implements Comparator { - - @Override - public int compare(UserId o1, UserId o2) { - if (o1 == o2) { - return 0; - } - if (o1 == null) { - return -1; - } - if (o2 == null) { - return 1; - } - - NullSafeStringComparator c = new NullSafeStringComparator(); - int cName = c.compare(o1.getName(), o2.getName()); - if (cName != 0) { - return cName; - } - - int cComment = c.compare(o1.getComment(), o2.getComment()); - if (cComment != 0) { - return cComment; - } - - return c.compare(o1.getEmail(), o2.getEmail()); - } - } - - public static class DefaultIgnoreCaseComparator implements Comparator { - - @Override - public int compare(UserId o1, UserId o2) { - if (o1 == o2) { - return 0; - } - if (o1 == null) { - return -1; - } - if (o2 == null) { - return 1; - } - - NullSafeStringComparator c = new NullSafeStringComparator(); - int cName = c.compare(lower(o1.getName()), lower(o2.getName())); - if (cName != 0) { - return cName; - } - - int cComment = c.compare(lower(o1.getComment()), lower(o2.getComment())); - if (cComment != 0) { - return cComment; - } - - return c.compare(lower(o1.getEmail()), lower(o2.getEmail())); - } - - private static String lower(String string) { - return string == null ? null : string.toLowerCase(); - } - } - - private static class NullSafeStringComparator implements Comparator { - - @Override - public int compare(String o1, String o2) { - // noinspection StringEquality - if (o1 == o2) { - return 0; - } - if (o1 == null) { - return -1; - } - if (o2 == null) { - return 1; - } - return o1.compareTo(o2); - } - } -} 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 3556f104..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/policy/Policy.java +++ /dev/null @@ -1,733 +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.Date; -import java.util.EnumMap; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.NoSuchElementException; - -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.DateUtil; -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.smartSignatureHashAlgorithmPolicy(); - private HashAlgorithmPolicy revocationSignatureHashAlgorithmPolicy = - HashAlgorithmPolicy.smartSignatureHashAlgorithmPolicy(); - private SymmetricKeyAlgorithmPolicy symmetricKeyEncryptionAlgorithmPolicy = - SymmetricKeyAlgorithmPolicy.symmetricKeyEncryptionPolicy2022(); - private SymmetricKeyAlgorithmPolicy symmetricKeyDecryptionAlgorithmPolicy = - SymmetricKeyAlgorithmPolicy.symmetricKeyDecryptionPolicy2022(); - private CompressionAlgorithmPolicy compressionAlgorithmPolicy = - CompressionAlgorithmPolicy.anyCompressionAlgorithmPolicy(); - private PublicKeyAlgorithmPolicy publicKeyAlgorithmPolicy = - PublicKeyAlgorithmPolicy.bsi2021PublicKeyAlgorithmPolicy(); - private final NotationRegistry notationRegistry = new NotationRegistry(); - - private AlgorithmSuite keyGenerationAlgorithmSuite = AlgorithmSuite.getDefaultAlgorithmSuite(); - - // Signers User-ID is soon to be deprecated. - private SignerUserIdValidationLevel signerUserIdValidationLevel = SignerUserIdValidationLevel.DISABLED; - - private boolean enableKeyParameterValidation = false; - - public enum SignerUserIdValidationLevel { - /** - * PGPainless will verify {@link 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 {@link org.bouncycastle.bcpg.sig.SignerUserID} subpackets on signature. - */ - DISABLED - } - - 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) { - try { - SymmetricKeyAlgorithm algorithm = SymmetricKeyAlgorithm.requireFromId(algorithmId); - return isAcceptable(algorithm); - } catch (NoSuchElementException e) { - // Unknown algorithm is not acceptable - return false; - } - } - - /** - * The default symmetric encryption algorithm policy of PGPainless. - * - * @return default symmetric encryption algorithm policy - * @deprecated not expressive - will be removed in a future release - */ - @Deprecated - public static SymmetricKeyAlgorithmPolicy defaultSymmetricKeyEncryptionAlgorithmPolicy() { - return 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 - */ - public static SymmetricKeyAlgorithmPolicy symmetricKeyEncryptionPolicy2022() { - return new SymmetricKeyAlgorithmPolicy(SymmetricKeyAlgorithm.AES_256, Arrays.asList( - // Reject: Unencrypted, IDEA, TripleDES, CAST5, Blowfish - 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 - */ - @Deprecated - public static SymmetricKeyAlgorithmPolicy defaultSymmetricKeyDecryptionAlgorithmPolicy() { - return 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 - */ - public static SymmetricKeyAlgorithmPolicy symmetricKeyDecryptionPolicy2022() { - return new SymmetricKeyAlgorithmPolicy(SymmetricKeyAlgorithm.AES_256, Arrays.asList( - // Reject: Unencrypted, IDEA, TripleDES, Blowfish - SymmetricKeyAlgorithm.CAST5, - SymmetricKeyAlgorithm.AES_256, - SymmetricKeyAlgorithm.AES_192, - SymmetricKeyAlgorithm.AES_128, - 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 Map acceptableHashAlgorithmsAndTerminationDates; - - /** - * Create a {@link HashAlgorithmPolicy} which accepts all {@link HashAlgorithm HashAlgorithms} 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 - */ - public HashAlgorithmPolicy(@Nonnull HashAlgorithm defaultHashAlgorithm, @Nonnull Map algorithmTerminationDates) { - this.defaultHashAlgorithm = defaultHashAlgorithm; - this.acceptableHashAlgorithmsAndTerminationDates = algorithmTerminationDates; - } - - /** - * Create a {@link HashAlgorithmPolicy} which accepts all {@link HashAlgorithm HashAlgorithms} 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 - */ - public HashAlgorithmPolicy(@Nonnull HashAlgorithm defaultHashAlgorithm, @Nonnull List acceptableHashAlgorithms) { - this(defaultHashAlgorithm, Collections.unmodifiableMap(listToMap(acceptableHashAlgorithms))); - } - - private static Map listToMap(@Nonnull List algorithms) { - Map algorithmsAndTerminationDates = new HashMap<>(); - for (HashAlgorithm algorithm : algorithms) { - algorithmsAndTerminationDates.put(algorithm, null); - } - return algorithmsAndTerminationDates; - } - - /** - * 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 given hash algorithm is currently acceptable by this policy. - * - * @param hashAlgorithm hash algorithm - * @return true if the hash algorithm is acceptable, false otherwise - */ - public boolean isAcceptable(@Nonnull HashAlgorithm hashAlgorithm) { - return isAcceptable(hashAlgorithm, new Date()); - } - - /** - * Return true if the given hash algorithm is currently acceptable by this policy. - * - * @param algorithmId hash algorithm - * @return true if the hash algorithm is acceptable, false otherwise - */ - public boolean isAcceptable(int algorithmId) { - try { - HashAlgorithm algorithm = HashAlgorithm.requireFromId(algorithmId); - return isAcceptable(algorithm); - } catch (NoSuchElementException e) { - // Unknown algorithm is not acceptable - return false; - } - } - - /** - * Return true, if the given algorithm is acceptable for the given usage date. - * - * @param hashAlgorithm algorithm - * @param usageDate usage date (e.g. signature creation time) - * - * @return acceptance - */ - public boolean isAcceptable(@Nonnull HashAlgorithm hashAlgorithm, @Nonnull Date usageDate) { - if (!acceptableHashAlgorithmsAndTerminationDates.containsKey(hashAlgorithm)) { - return false; - } - - // Check termination date - Date terminationDate = acceptableHashAlgorithmsAndTerminationDates.get(hashAlgorithm); - if (terminationDate == null) { - return true; - } - - // Reject if usage date is past termination date - return terminationDate.after(usageDate); - } - - public boolean isAcceptable(int algorithmId, @Nonnull Date usageDate) { - try { - HashAlgorithm algorithm = HashAlgorithm.requireFromId(algorithmId); - return isAcceptable(algorithm, usageDate); - } catch (NoSuchElementException e) { - // Unknown algorithm is not acceptable - return false; - } - } - - /** - * 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 - * @deprecated not expressive - will be removed in an upcoming release - */ - @Deprecated - public static HashAlgorithmPolicy defaultSignatureAlgorithmPolicy() { - return smartSignatureHashAlgorithmPolicy(); - } - - /** - * {@link HashAlgorithmPolicy} which takes the date of the algorithm usage into consideration. - * If the policy has a termination date for a given algorithm, and the usage date is after that termination - * date, the algorithm is rejected. - * - * This policy is inspired by Sequoia-PGP's collision resistant algorithm policy. - * - * @see Sequoia-PGP's Collision Resistant Algorithm Policy - * - * @return smart signature algorithm policy - */ - public static HashAlgorithmPolicy smartSignatureHashAlgorithmPolicy() { - Map algorithmDateMap = new HashMap<>(); - - algorithmDateMap.put(HashAlgorithm.MD5, DateUtil.parseUTCDate("1997-02-01 00:00:00 UTC")); - algorithmDateMap.put(HashAlgorithm.SHA1, DateUtil.parseUTCDate("2013-02-01 00:00:00 UTC")); - algorithmDateMap.put(HashAlgorithm.RIPEMD160, DateUtil.parseUTCDate("2013-02-01 00:00:00 UTC")); - algorithmDateMap.put(HashAlgorithm.SHA224, null); - algorithmDateMap.put(HashAlgorithm.SHA256, null); - algorithmDateMap.put(HashAlgorithm.SHA384, null); - algorithmDateMap.put(HashAlgorithm.SHA512, null); - - return new HashAlgorithmPolicy(HashAlgorithm.SHA512, algorithmDateMap); - } - - /** - * {@link HashAlgorithmPolicy} which only accepts signatures made using algorithms which are acceptable - * according to 2022 standards. - * - * Particularly this policy only accepts algorithms from the SHA2 family. - * - * @return static signature algorithm policy - */ - public static HashAlgorithmPolicy static2022SignatureHashAlgorithmPolicy() { - 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 - * @deprecated not expressive - will be removed in an upcoming release - */ - @Deprecated - public static HashAlgorithmPolicy defaultRevocationSignatureHashAlgorithmPolicy() { - return smartSignatureHashAlgorithmPolicy(); - } - - /** - * Hash algorithm policy for revocation signatures, which accepts SHA1 and SHA2 algorithms, as well as RIPEMD160. - * - * @return static revocation signature hash algorithm policy - */ - public static HashAlgorithmPolicy static2022RevocationSignatureHashAlgorithmPolicy() { - 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) { - try { - CompressionAlgorithm compressionAlgorithm = CompressionAlgorithm.requireFromId(compressionAlgorithmTag); - return isAcceptable(compressionAlgorithm); - } catch (NoSuchElementException e) { - // Unknown algorithm is not acceptable - return false; - } - } - - public boolean isAcceptable(CompressionAlgorithm compressionAlgorithm) { - return acceptableCompressionAlgorithms.contains(compressionAlgorithm); - } - - /** - * Default {@link 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 - */ - @Deprecated - public static CompressionAlgorithmPolicy defaultCompressionAlgorithmPolicy() { - return anyCompressionAlgorithmPolicy(); - } - - /** - * Policy that accepts any known compression algorithm and offers {@link CompressionAlgorithm#ZIP} as - * default algorithm. - * - * @return compression algorithm policy - */ - public static CompressionAlgorithmPolicy anyCompressionAlgorithmPolicy() { - 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) { - try { - PublicKeyAlgorithm algorithm = PublicKeyAlgorithm.requireFromId(algorithmId); - return isAcceptable(algorithm, bitStrength); - } catch (NoSuchElementException e) { - // Unknown algorithm is not acceptable - return false; - } - } - - 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). - * - * @return default algorithm policy - * @deprecated not expressive - might be removed in a future release - */ - @Deprecated - public static PublicKeyAlgorithmPolicy defaultPublicKeyAlgorithmPolicy() { - return 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. - * - * @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 bsi2021PublicKeyAlgorithmPolicy() { - 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; - } - - /** - * Return the level of validation PGPainless shall do on {@link org.bouncycastle.bcpg.sig.SignerUserID} subpackets. - * By default, this value is {@link SignerUserIdValidationLevel#DISABLED}. - * - * @return the level of validation - */ - public SignerUserIdValidationLevel getSignerUserIdValidationLevel() { - return signerUserIdValidationLevel; - } - - /** - * Specify, how {@link org.bouncycastle.bcpg.sig.SignerUserID} subpackets on signatures shall be validated. - * - * @param signerUserIdValidationLevel level of verification PGPainless shall do on - * {@link org.bouncycastle.bcpg.sig.SignerUserID} subpackets. - * @return policy instance - */ - public Policy setSignerUserIdValidationLevel(SignerUserIdValidationLevel signerUserIdValidationLevel) { - if (signerUserIdValidationLevel == null) { - throw new NullPointerException("SignerUserIdValidationLevel cannot be null."); - } - this.signerUserIdValidationLevel = signerUserIdValidationLevel; - return this; - } - - /** - * Enable or disable validation of public key parameters when unlocking private keys. - * Disabled by default. - * When enabled, PGPainless will validate, whether public key parameters have been tampered with. - * This is a countermeasure against possible attacks described in the paper - * "Victory by KO: Attacking OpenPGP Using Key Overwriting" by Lara Bruseghini, Daniel Huigens, and Kenneth G. Paterson. - * Since these attacks are only possible in very special conditions (attacker has access to the encrypted private key), - * and the countermeasures are very costly, they are disabled by default, but can be enabled using this method. - * - * @see KOpenPGP.com - * @param enable boolean - * @return this - */ - public Policy setEnableKeyParameterValidation(boolean enable) { - this.enableKeyParameterValidation = enable; - return this; - } - - /** - * Return true, if countermeasures against the KOpenPGP attacks are enabled, false otherwise. - * - * @return true if countermeasures are enabled, false otherwise. - */ - public boolean isEnableKeyParameterValidation() { - return enableKeyParameterValidation; - } -} 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/SignatureUtils.java b/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureUtils.java deleted file mode 100644 index 2b69a832..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureUtils.java +++ /dev/null @@ -1,370 +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.Collections; -import java.util.Date; -import java.util.Iterator; -import java.util.List; -import java.util.Set; -import javax.annotation.Nonnull; - -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.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.bouncycastle.util.io.Streams; -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.OpenPgpFingerprint; -import org.pgpainless.key.util.OpenPgpKeyAttributeUtil; -import org.pgpainless.key.util.RevocationAttributes; -import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil; -import org.pgpainless.util.ArmorUtils; - -/** - * Utility methods related to signatures. - */ -public final class SignatureUtils { - - public static final int MAX_ITERATIONS = 10000; - - 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}. - * - * @param publicKey public key - * @return content signer builder - */ - public static PGPContentSignerBuilder getPgpContentSignerBuilderForKey(PGPPublicKey publicKey) { - Set hashAlgorithmSet = OpenPgpKeyAttributeUtil.getOrGuessPreferredHashAlgorithms(publicKey); - - HashAlgorithm hashAlgorithm = HashAlgorithmNegotiator.negotiateSignatureHashAlgorithm(PGPainless.getPolicy()) - .negotiateHashAlgorithm(hashAlgorithmSet); - - return ImplementationFactory.getInstance().getPGPContentSignerBuilder(publicKey.getAlgorithm(), hashAlgorithm.getAlgorithmId()); - } - - /** - * 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 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' - */ - 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 - * @throws PGPException in case of a broken signature - */ - public static List readSignatures(String encodedSignatures) throws IOException, PGPException { - @SuppressWarnings("CharsetObjectCanBeUsed") - Charset utf8 = Charset.forName("UTF-8"); - byte[] bytes = encodedSignatures.getBytes(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 { - return readSignatures(inputStream, MAX_ITERATIONS); - } - - /** - * Read and return {@link PGPSignature PGPSignatures}. - * 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 - * @throws IOException in case of a stream error - * @throws PGPException in case of an OpenPGP error - */ - public static List readSignatures(InputStream inputStream, int maxIterations) throws IOException, PGPException { - List signatures = new ArrayList<>(); - InputStream pgpIn = ArmorUtils.getDecoderStream(inputStream); - PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(pgpIn); - - int i = 0; - Object nextObject; - while (i++ < maxIterations && (nextObject = objectFactory.nextObject()) != 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 instanceof PGPCompressedData) { - PGPCompressedData compressedData = (PGPCompressedData) nextObject; - // getInputStream() does not do decompression, contrary to getDataStream(). - Streams.drain(compressedData.getInputStream()); // Skip packet without decompressing - } - - if (nextObject instanceof PGPSignatureList) { - PGPSignatureList signatureList = (PGPSignatureList) nextObject; - for (PGPSignature s : signatureList) { - signatures.add(s); - } - } - - if (nextObject instanceof PGPSignature) { - signatures.add((PGPSignature) nextObject); - } - } - pgpIn.close(); - - return signatures; - } - - /** - * 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) { - if (signature.getVersion() == 3) { - // V3 sigs do not contain subpackets - return signature.getKeyID(); - } - - IssuerKeyID issuerKeyId = SignatureSubpacketsUtil.getIssuerKeyId(signature); - OpenPgpFingerprint fingerprint = SignatureSubpacketsUtil.getIssuerFingerprintAsOpenPgpFingerprint(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; - } - - public static boolean wasIssuedBy(byte[] fingerprint, PGPSignature signature) { - try { - OpenPgpFingerprint fp = OpenPgpFingerprint.parseFromBinary(fingerprint); - OpenPgpFingerprint issuerFp = SignatureSubpacketsUtil.getIssuerFingerprintAsOpenPgpFingerprint(signature); - if (issuerFp == null) { - return fp.getKeyId() == signature.getKeyID(); - } - return fp.equals(issuerFp); - } catch (IllegalArgumentException e) { - // Unknown fingerprint length - return false; - } - } - - /** - * Extract all signatures from the given
key
which were issued by
issuerKeyId
- * over
userId
. - * - * @param key public key - * @param userId user-id - * @param issuerKeyId issuer key-id - * @return (potentially empty) list of signatures - */ - public static @Nonnull List getSignaturesOverUserIdBy( - @Nonnull PGPPublicKey key, - @Nonnull String userId, - long issuerKeyId) { - List signaturesByKeyId = new ArrayList<>(); - Iterator userIdSignatures = key.getSignaturesForID(userId); - - // getSignaturesForID() is nullable for some reason -.- - if (userIdSignatures == null) { - return signaturesByKeyId; - } - - // filter for signatures by key-id - while (userIdSignatures.hasNext()) { - PGPSignature signature = userIdSignatures.next(); - if (signature.getKeyID() == issuerKeyId) { - signaturesByKeyId.add(signature); - } - } - - return Collections.unmodifiableList(signaturesByKeyId); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/AbstractSignatureBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/AbstractSignatureBuilder.java deleted file mode 100644 index 1079f92f..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/AbstractSignatureBuilder.java +++ /dev/null @@ -1,140 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature.builder; - -import java.util.Set; -import javax.annotation.Nonnull; - -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; - -public abstract class AbstractSignatureBuilder> { - protected final PGPPrivateKey privateSigningKey; - protected final PGPPublicKey publicSigningKey; - - protected HashAlgorithm hashAlgorithm; - protected SignatureType signatureType; - - protected SignatureSubpackets unhashedSubpackets; - protected SignatureSubpackets hashedSubpackets; - - protected AbstractSignatureBuilder(SignatureType signatureType, - PGPSecretKey signingKey, - SecretKeyRingProtector protector, - HashAlgorithm hashAlgorithm, - SignatureSubpackets hashedSubpackets, - SignatureSubpackets unhashedSubpackets) - throws PGPException { - if (!isValidSignatureType(signatureType)) { - throw new IllegalArgumentException("Invalid signature type."); - } - this.signatureType = signatureType; - this.privateSigningKey = UnlockSecretKey.unlockSecretKey(signingKey, protector); - this.publicSigningKey = signingKey.getPublicKey(); - this.hashAlgorithm = hashAlgorithm; - this.hashedSubpackets = hashedSubpackets; - this.unhashedSubpackets = unhashedSubpackets; - } - - public AbstractSignatureBuilder(SignatureType signatureType, PGPSecretKey signingKey, SecretKeyRingProtector protector) - throws PGPException { - this( - signatureType, - signingKey, - protector, - negotiateHashAlgorithm(signingKey.getPublicKey()), - SignatureSubpackets.createHashedSubpackets(signingKey.getPublicKey()), - SignatureSubpackets.createEmptySubpackets() - ); - } - - public AbstractSignatureBuilder(PGPSecretKey certificationKey, SecretKeyRingProtector protector, PGPSignature archetypeSignature) - throws PGPException { - this( - SignatureType.valueOf(archetypeSignature.getSignatureType()), - certificationKey, - protector, - negotiateHashAlgorithm(certificationKey.getPublicKey()), - SignatureSubpackets.refreshHashedSubpackets(certificationKey.getPublicKey(), archetypeSignature), - SignatureSubpackets.refreshUnhashedSubpackets(archetypeSignature) - ); - } - - /** - * Negotiate a {@link HashAlgorithm} to be used when creating the signature. - * - * @param publicKey signing public key - * @return hash algorithm - */ - protected static HashAlgorithm negotiateHashAlgorithm(PGPPublicKey publicKey) { - Set hashAlgorithmPreferences = OpenPgpKeyAttributeUtil.getOrGuessPreferredHashAlgorithms(publicKey); - return HashAlgorithmNegotiator.negotiateSignatureHashAlgorithm(PGPainless.getPolicy()) - .negotiateHashAlgorithm(hashAlgorithmPreferences); - } - - public B overrideHashAlgorithm(@Nonnull HashAlgorithm hashAlgorithm) { - this.hashAlgorithm = hashAlgorithm; - return (B) this; - } - - /** - * Set the builders {@link SignatureType}. - * Note that only those types who are valid for the concrete subclass of this {@link AbstractSignatureBuilder} - * are allowed. Invalid choices result in an {@link IllegalArgumentException} to be thrown. - * - * @param type signature type - * @return builder - */ - public B setSignatureType(SignatureType type) { - if (!isValidSignatureType(type)) { - throw new IllegalArgumentException("Invalid signature type: " + type); - } - this.signatureType = type; - return (B) this; - } - - /** - * Build an instance of {@link 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 - */ - protected PGPSignatureGenerator buildAndInitSignatureGenerator() throws PGPException { - PGPSignatureGenerator generator = new PGPSignatureGenerator( - ImplementationFactory.getInstance().getPGPContentSignerBuilder( - publicSigningKey.getAlgorithm(), hashAlgorithm.getAlgorithmId() - ) - ); - generator.setUnhashedSubpackets(SignatureSubpacketsHelper.toVector(unhashedSubpackets)); - generator.setHashedSubpackets(SignatureSubpacketsHelper.toVector(hashedSubpackets)); - generator.init(signatureType.getCode(), privateSigningKey); - return generator; - } - - /** - * Return true if the given {@link SignatureType} is a valid choice for the concrete implementation - * of {@link AbstractSignatureBuilder}. - * - * @param type type - * @return return true if valid, false otherwise - */ - protected abstract boolean isValidSignatureType(SignatureType type); -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/DirectKeySelfSignatureBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/DirectKeySelfSignatureBuilder.java deleted file mode 100644 index cf99f8d6..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/DirectKeySelfSignatureBuilder.java +++ /dev/null @@ -1,57 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature.builder; - -import javax.annotation.Nullable; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKey; -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.SelfSignatureSubpackets; - -public class DirectKeySelfSignatureBuilder extends AbstractSignatureBuilder { - - public DirectKeySelfSignatureBuilder(PGPSecretKey certificationKey, SecretKeyRingProtector protector, PGPSignature archetypeSignature) - throws PGPException { - super(certificationKey, protector, archetypeSignature); - } - - public DirectKeySelfSignatureBuilder(PGPSecretKey signingKey, SecretKeyRingProtector protector) throws PGPException { - super(SignatureType.DIRECT_KEY, signingKey, protector); - } - - public SelfSignatureSubpackets getHashedSubpackets() { - return hashedSubpackets; - } - - public SelfSignatureSubpackets getUnhashedSubpackets() { - return unhashedSubpackets; - } - - public void applyCallback(@Nullable SelfSignatureSubpackets.Callback callback) { - if (callback != null) { - callback.modifyHashedSubpackets(getHashedSubpackets()); - callback.modifyUnhashedSubpackets(getUnhashedSubpackets()); - } - } - - public PGPSignature build(PGPPublicKey key) throws PGPException { - PGPSignatureGenerator signatureGenerator = buildAndInitSignatureGenerator(); - if (key.getKeyID() != publicSigningKey.getKeyID()) { - return signatureGenerator.generateCertification(publicSigningKey, key); - } else { - return signatureGenerator.generateCertification(key); - } - } - - @Override - protected boolean isValidSignatureType(SignatureType type) { - return type == SignatureType.DIRECT_KEY; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/PrimaryKeyBindingSignatureBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/PrimaryKeyBindingSignatureBuilder.java deleted file mode 100644 index 156e739f..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/PrimaryKeyBindingSignatureBuilder.java +++ /dev/null @@ -1,59 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature.builder; - -import javax.annotation.Nullable; - -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; - -public class PrimaryKeyBindingSignatureBuilder extends AbstractSignatureBuilder { - - public PrimaryKeyBindingSignatureBuilder(PGPSecretKey subkey, SecretKeyRingProtector subkeyProtector) - throws PGPException { - super(SignatureType.PRIMARYKEY_BINDING, subkey, subkeyProtector); - } - - public PrimaryKeyBindingSignatureBuilder(PGPSecretKey secretSubKey, - SecretKeyRingProtector subkeyProtector, - HashAlgorithm hashAlgorithm) - throws PGPException { - super(SignatureType.PRIMARYKEY_BINDING, secretSubKey, subkeyProtector, hashAlgorithm, - SignatureSubpackets.createHashedSubpackets(secretSubKey.getPublicKey()), - SignatureSubpackets.createEmptySubpackets()); - } - - public SelfSignatureSubpackets getHashedSubpackets() { - return hashedSubpackets; - } - - public SelfSignatureSubpackets getUnhashedSubpackets() { - return unhashedSubpackets; - } - - public void applyCallback(@Nullable SelfSignatureSubpackets.Callback callback) { - if (callback != null) { - callback.modifyHashedSubpackets(getHashedSubpackets()); - callback.modifyUnhashedSubpackets(getUnhashedSubpackets()); - } - } - - @Override - protected boolean isValidSignatureType(SignatureType type) { - return type == SignatureType.PRIMARYKEY_BINDING; - } - - public PGPSignature build(PGPPublicKey primaryKey) throws PGPException { - return buildAndInitSignatureGenerator() - .generateCertification(primaryKey, publicSigningKey); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/RevocationSignatureBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/RevocationSignatureBuilder.java deleted file mode 100644 index 3c2dcab9..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/RevocationSignatureBuilder.java +++ /dev/null @@ -1,71 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature.builder; - -import javax.annotation.Nullable; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKey; -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.RevocationSignatureSubpackets; - -public class RevocationSignatureBuilder extends AbstractSignatureBuilder { - - public RevocationSignatureBuilder(SignatureType signatureType, PGPSecretKey signingKey, SecretKeyRingProtector protector) throws PGPException { - super(signatureType, signingKey, protector); - getHashedSubpackets().setRevocable(false); - } - - @Override - protected boolean isValidSignatureType(SignatureType type) { - switch (type) { - case KEY_REVOCATION: - case SUBKEY_REVOCATION: - case CERTIFICATION_REVOCATION: - return true; - default: - return false; - } - } - - public RevocationSignatureSubpackets getHashedSubpackets() { - return hashedSubpackets; - } - - public RevocationSignatureSubpackets getUnhashedSubpackets() { - return unhashedSubpackets; - } - - public void applyCallback(@Nullable RevocationSignatureSubpackets.Callback callback) { - if (callback != null) { - callback.modifyHashedSubpackets(getHashedSubpackets()); - callback.modifyUnhashedSubpackets(getUnhashedSubpackets()); - } - } - - public PGPSignature build(PGPPublicKey revokeeSubkey) throws PGPException { - PGPSignatureGenerator signatureGenerator = buildAndInitSignatureGenerator(); - if (signatureType == SignatureType.KEY_REVOCATION) { - if (!revokeeSubkey.isMasterKey()) { - throw new IllegalArgumentException("Signature type is KEY_REVOCATION, but provided revokeeSubkey does not appear to be a primary key."); - } - return signatureGenerator.generateCertification(publicSigningKey); - } else { - return signatureGenerator.generateCertification(publicSigningKey, revokeeSubkey); - } - } - - public PGPSignature build(String revokeeUserId) throws PGPException { - PGPSignatureGenerator signatureGenerator = buildAndInitSignatureGenerator(); - if (signatureType != SignatureType.CERTIFICATION_REVOCATION) { - throw new IllegalArgumentException("Signature type is != CERTIFICATION_REVOCATION."); - } - return signatureGenerator.generateCertification(revokeeUserId, publicSigningKey); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/SelfSignatureBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/SelfSignatureBuilder.java deleted file mode 100644 index e6bf94c3..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/SelfSignatureBuilder.java +++ /dev/null @@ -1,74 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature.builder; - -import javax.annotation.Nullable; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKey; -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; - -public class SelfSignatureBuilder extends AbstractSignatureBuilder { - - public SelfSignatureBuilder(PGPSecretKey signingKey, SecretKeyRingProtector protector) throws PGPException { - this(SignatureType.GENERIC_CERTIFICATION, signingKey, protector); - } - - public SelfSignatureBuilder(SignatureType signatureType, PGPSecretKey signingKey, SecretKeyRingProtector protector) - throws PGPException { - super(signatureType, signingKey, protector); - } - - public SelfSignatureBuilder( - PGPSecretKey primaryKey, - SecretKeyRingProtector primaryKeyProtector, - PGPSignature oldCertification) - throws PGPException { - super(primaryKey, primaryKeyProtector, oldCertification); - } - - public SelfSignatureSubpackets getHashedSubpackets() { - return hashedSubpackets; - } - - public SelfSignatureSubpackets getUnhashedSubpackets() { - return unhashedSubpackets; - } - - public void applyCallback(@Nullable SelfSignatureSubpackets.Callback callback) { - if (callback != null) { - callback.modifyHashedSubpackets(getHashedSubpackets()); - callback.modifyUnhashedSubpackets(getUnhashedSubpackets()); - } - } - - public PGPSignature build(PGPPublicKey certifiedKey, String userId) throws PGPException { - return buildAndInitSignatureGenerator().generateCertification(userId, certifiedKey); - } - - public PGPSignature build(PGPPublicKey certifiedKey, PGPUserAttributeSubpacketVector userAttribute) - throws PGPException { - return buildAndInitSignatureGenerator().generateCertification(userAttribute, certifiedKey); - } - - @Override - protected boolean isValidSignatureType(SignatureType type) { - switch (type) { - case GENERIC_CERTIFICATION: - case NO_CERTIFICATION: - case CASUAL_CERTIFICATION: - case POSITIVE_CERTIFICATION: - case DIRECT_KEY: - return true; - default: - return false; - } - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/SubkeyBindingSignatureBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/SubkeyBindingSignatureBuilder.java deleted file mode 100644 index c15e219e..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/SubkeyBindingSignatureBuilder.java +++ /dev/null @@ -1,72 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature.builder; - -import javax.annotation.Nullable; - -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; - -public class SubkeyBindingSignatureBuilder extends AbstractSignatureBuilder { - - public SubkeyBindingSignatureBuilder(PGPSecretKey signingKey, SecretKeyRingProtector protector) - throws PGPException { - super(SignatureType.SUBKEY_BINDING, signingKey, protector); - } - - public SubkeyBindingSignatureBuilder(PGPSecretKey signingKey, SecretKeyRingProtector protector, HashAlgorithm hashAlgorithm) - throws PGPException { - super(SignatureType.SUBKEY_BINDING, signingKey, protector, hashAlgorithm, - SignatureSubpackets.createHashedSubpackets(signingKey.getPublicKey()), - SignatureSubpackets.createEmptySubpackets()); - } - - public SubkeyBindingSignatureBuilder( - PGPSecretKey signingKey, - SecretKeyRingProtector protector, - PGPSignature oldSubkeyBinding) - throws PGPException { - super(signingKey, protector, requireValidSignatureType(oldSubkeyBinding)); - } - - private static PGPSignature requireValidSignatureType(PGPSignature signature) { - if (signature.getSignatureType() == SignatureType.SUBKEY_BINDING.getCode()) { - return signature; - } - throw new IllegalArgumentException("Invalid signature type."); - } - - @Override - protected boolean isValidSignatureType(SignatureType type) { - return type == SignatureType.SUBKEY_BINDING; - } - - public SelfSignatureSubpackets getHashedSubpackets() { - return hashedSubpackets; - } - - public SelfSignatureSubpackets getUnhashedSubpackets() { - return unhashedSubpackets; - } - - public void applyCallback(@Nullable SelfSignatureSubpackets.Callback callback) { - if (callback != null) { - callback.modifyHashedSubpackets(getHashedSubpackets()); - callback.modifyUnhashedSubpackets(getUnhashedSubpackets()); - } - } - - public PGPSignature build(PGPPublicKey subkey) throws PGPException { - return buildAndInitSignatureGenerator() - .generateCertification(publicSigningKey, subkey); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/ThirdPartyCertificationSignatureBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/ThirdPartyCertificationSignatureBuilder.java deleted file mode 100644 index d75e269d..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/ThirdPartyCertificationSignatureBuilder.java +++ /dev/null @@ -1,119 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature.builder; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -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.exception.WrongPassphraseException; -import org.pgpainless.key.protection.SecretKeyRingProtector; -import org.pgpainless.signature.subpackets.CertificationSubpackets; - -/** - * Certification signature builder used to certify other users keys. - */ -public class ThirdPartyCertificationSignatureBuilder extends AbstractSignatureBuilder { - - /** - * Create a new certification signature builder. - * This constructor uses {@link 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 - */ - public ThirdPartyCertificationSignatureBuilder(PGPSecretKey signingKey, SecretKeyRingProtector protector) - throws PGPException { - this(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 - */ - public ThirdPartyCertificationSignatureBuilder(SignatureType signatureType, PGPSecretKey signingKey, SecretKeyRingProtector protector) - throws PGPException { - 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 - */ - public ThirdPartyCertificationSignatureBuilder( - PGPSecretKey signingKey, - SecretKeyRingProtector protector, - PGPSignature archetypeSignature) - throws PGPException { - super(signingKey, protector, archetypeSignature); - } - - public CertificationSubpackets getHashedSubpackets() { - return hashedSubpackets; - } - - public CertificationSubpackets getUnhashedSubpackets() { - return unhashedSubpackets; - } - - public void applyCallback(@Nullable CertificationSubpackets.Callback callback) { - if (callback != null) { - callback.modifyHashedSubpackets(getHashedSubpackets()); - callback.modifyUnhashedSubpackets(getUnhashedSubpackets()); - } - } - - /** - * Create a certification signature for the given user-id and the primary key of the given key ring. - * @param certifiedKey key ring - * @param userId user-id to certify - * @return signature - * - * @throws PGPException if the signature generator cannot be initialized - */ - public PGPSignature build(PGPPublicKeyRing certifiedKey, String userId) throws PGPException { - return buildAndInitSignatureGenerator().generateCertification(userId, certifiedKey.getPublicKey()); - } - - /** - * Create a certification signature for the given user attribute and the primary key of the given key ring. - * @param certifiedKey key ring - * @param userAttribute user-attributes to certify - * @return signature - * - * @throws PGPException if the signature generator cannot be initialized - */ - public PGPSignature build(PGPPublicKeyRing certifiedKey, PGPUserAttributeSubpacketVector userAttribute) - throws PGPException { - return buildAndInitSignatureGenerator().generateCertification(userAttribute, certifiedKey.getPublicKey()); - } - - @Override - protected boolean isValidSignatureType(@Nonnull SignatureType type) { - switch (type) { - case GENERIC_CERTIFICATION: - case NO_CERTIFICATION: - case CASUAL_CERTIFICATION: - case POSITIVE_CERTIFICATION: - return true; - default: - return false; - } - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/ThirdPartyDirectKeySignatureBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/ThirdPartyDirectKeySignatureBuilder.java deleted file mode 100644 index dd720bce..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/ThirdPartyDirectKeySignatureBuilder.java +++ /dev/null @@ -1,57 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature.builder; - -import javax.annotation.Nullable; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKey; -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.CertificationSubpackets; - -public class ThirdPartyDirectKeySignatureBuilder extends AbstractSignatureBuilder { - - public ThirdPartyDirectKeySignatureBuilder(PGPSecretKey certificationKey, SecretKeyRingProtector protector, PGPSignature archetypeSignature) - throws PGPException { - super(certificationKey, protector, archetypeSignature); - } - - public ThirdPartyDirectKeySignatureBuilder(PGPSecretKey signingKey, SecretKeyRingProtector protector) throws PGPException { - super(SignatureType.DIRECT_KEY, signingKey, protector); - } - - public CertificationSubpackets getHashedSubpackets() { - return hashedSubpackets; - } - - public CertificationSubpackets getUnhashedSubpackets() { - return unhashedSubpackets; - } - - public void applyCallback(@Nullable CertificationSubpackets.Callback callback) { - if (callback != null) { - callback.modifyHashedSubpackets(getHashedSubpackets()); - callback.modifyUnhashedSubpackets(getUnhashedSubpackets()); - } - } - - public PGPSignature build(PGPPublicKey key) throws PGPException { - PGPSignatureGenerator signatureGenerator = buildAndInitSignatureGenerator(); - if (key.getKeyID() != publicSigningKey.getKeyID()) { - return signatureGenerator.generateCertification(publicSigningKey, key); - } else { - return signatureGenerator.generateCertification(key); - } - } - - @Override - protected boolean isValidSignatureType(SignatureType type) { - return type == SignatureType.DIRECT_KEY; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/UniversalSignatureBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/UniversalSignatureBuilder.java deleted file mode 100644 index 7ca512bf..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/UniversalSignatureBuilder.java +++ /dev/null @@ -1,55 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature.builder; - -import javax.annotation.Nullable; - -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. - */ -public class UniversalSignatureBuilder extends AbstractSignatureBuilder { - - public UniversalSignatureBuilder(SignatureType signatureType, PGPSecretKey signingKey, SecretKeyRingProtector protector) - throws PGPException { - super(signatureType, signingKey, protector); - } - - public UniversalSignatureBuilder(PGPSecretKey certificationKey, SecretKeyRingProtector protector, PGPSignature archetypeSignature) - throws PGPException { - super(certificationKey, protector, archetypeSignature); - } - - @Override - protected boolean isValidSignatureType(SignatureType type) { - return true; - } - - public SignatureSubpackets getHashedSubpackets() { - return hashedSubpackets; - } - - public SignatureSubpackets getUnhashedSubpackets() { - return unhashedSubpackets; - } - - public void applyCallback(@Nullable SignatureSubpackets.Callback callback) { - if (callback != null) { - callback.modifyHashedSubpackets(getHashedSubpackets()); - callback.modifyUnhashedSubpackets(getUnhashedSubpackets()); - } - } - - public PGPSignatureGenerator getSignatureGenerator() throws PGPException { - return buildAndInitSignatureGenerator(); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/package-info.java deleted file mode 100644 index c82eb0ec..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Classes related to OpenPGP signatures. - */ -package org.pgpainless.signature.builder; diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/CertificateValidator.java b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/CertificateValidator.java deleted file mode 100644 index c629de2d..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/CertificateValidator.java +++ /dev/null @@ -1,298 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature.consumer; - -import static org.pgpainless.signature.consumer.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.SignatureUtils; -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(); - if (revocation.getKeyID() != primaryKey.getKeyID()) { - // Revocation was not made by primary key, skip - // TODO: What about external revocation keys? - } - 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(); - if (keySignature.getKeyID() != primaryKey.getKeyID()) { - // Signature was not made by primary key, skip - continue; - } - 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(); - if (userIdSig.getKeyID() != primaryKey.getKeyID()) { - // Sig was made by external key, skip - continue; - } - 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; - boolean hasAnyUserIds = !userIdSignatures.keySet().isEmpty(); - 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 (hasAnyUserIds && !anyUserIdValid) { - throw new SignatureValidationException("No valid user-id found.", rejections); - } - - // Specific signer user-id - SignerUserID signerUserID = SignatureSubpacketsUtil.getSignerUserID(signature); - if (signerUserID != null && policy.getSignerUserIdValidationLevel() == Policy.SignerUserIdValidationLevel.STRICT) { - List signerUserIdSigs = userIdSignatures.get(signerUserID.getID()); - if (signerUserIdSigs == null || signerUserIdSigs.isEmpty()) { - throw new SignatureValidationException("Signature was allegedly made by user-id '" + signerUserID.getID() + - "' but we have no valid signatures for that on the certificate."); - } - - PGPSignature userIdSig = signerUserIdSigs.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()) { - PGPSignature directKeySignature = directKeySignatures.get(0); - KeyFlags keyFlags = SignatureSubpacketsUtil.getKeyFlags(directKeySignature); - if (keyFlags != null && KeyFlag.hasKeyFlag(keyFlags.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(); - if (revocation.getKeyID() != primaryKey.getKeyID()) { - // Subkey Revocation was not made by primary key, skip - continue; - } - 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 (directKeyFlags == null || !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/consumer/OnePassSignatureCheck.java b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/OnePassSignatureCheck.java deleted file mode 100644 index 7a6a5b10..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/OnePassSignatureCheck.java +++ /dev/null @@ -1,72 +0,0 @@ -// SPDX-FileCopyrightText: 2020 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 {@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 an identifier for 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/consumer/SignatureCheck.java b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureCheck.java deleted file mode 100644 index bc9f1f0b..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureCheck.java +++ /dev/null @@ -1,73 +0,0 @@ -// SPDX-FileCopyrightText: 2020 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.OpenPgpFingerprint; -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. - */ -public class SignatureCheck { - private final PGPSignature signature; - private final PGPKeyRing signingKeyRing; - private final SubkeyIdentifier signingKeyIdentifier; - - /** - * Create a new {@link SignatureCheck} object. - * - * @param signature signature - * @param signingKeyRing signing key that created the signature - * @param signingKeyIdentifier identifier of the used signing key - */ - public SignatureCheck(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 OpenPgpFingerprint} of the key that created the signature. - * - * @return fingerprint of the signing key - * @deprecated use {@link #getSigningKeyIdentifier()} instead. - * - * TODO: Remove in 1.2.X - */ - @Deprecated - public OpenPgpFingerprint getFingerprint() { - return signingKeyIdentifier.getSubkeyFingerprint(); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureCreationDateComparator.java b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureCreationDateComparator.java deleted file mode 100644 index 7996e33b..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureCreationDateComparator.java +++ /dev/null @@ -1,53 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature.consumer; - -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/consumer/SignaturePicker.java b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignaturePicker.java deleted file mode 100644 index e6f2f755..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignaturePicker.java +++ /dev/null @@ -1,395 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature.consumer; - -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.signature.SignatureUtils; -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 - * @param policy policy - * @param validationDate date of signature validation - * @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 policy policy - * @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 policy policy - * @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 policy policy - * @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 policy policy - * @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 policy policy - * @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) { - PGPPublicKey signer = keyRing.getPublicKey(signature.getKeyID()); - if (signer == null) { - // Signature made by external key. Skip. - continue; - } - 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 policy policy - * @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) { - if (primaryKey.getKeyID() != signature.getKeyID()) { - // Signature not made by primary key - continue; - } - 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 policy policy - * @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); - 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 policy policy - * @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 policy policy - * @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 policy policy - * @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.signatureDoesNotPredateSignee(subkey).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/consumer/SignatureValidator.java b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureValidator.java deleted file mode 100644 index cf0dc1fb..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureValidator.java +++ /dev/null @@ -1,674 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature.consumer; - -import java.util.Arrays; -import java.util.Date; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.NoSuchElementException; -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.OpenPgpFingerprint; -import org.pgpainless.policy.Policy; -import org.pgpainless.signature.SignatureUtils; -import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil; -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 { - OpenPgpFingerprint signingKeyFingerprint = OpenPgpFingerprint.of(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) + ")"); - } - } - - OpenPgpFingerprint fingerprint = - SignatureSubpacketsUtil.getIssuerFingerprintAsOpenPgpFingerprint(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 referenceDate reference date for signature verification - * @return validator - */ - public static SignatureValidator hasValidPrimaryKeyBindingSignatureIfRequired(PGPPublicKey primaryKey, - PGPPublicKey subkey, Policy policy, - Date referenceDate) { - return new SignatureValidator() { - @Override - public void verify(PGPSignature signature) throws SignatureValidationException { - if (!PublicKeyAlgorithm.requireFromId(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(referenceDate).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); - if (signature.getVersion() >= 4) { - 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.requireFromId(signingKey.getAlgorithm()); - int bitStrength = signingKey.getBitStrength(); - if (bitStrength == -1) { - throw new SignatureValidationException("Cannot determine bit strength of signing key."); - } - 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."); - } - } - }; - } - - /** - * 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 { - try { - HashAlgorithm hashAlgorithm = HashAlgorithm.requireFromId(signature.getHashAlgorithm()); - Policy.HashAlgorithmPolicy hashAlgorithmPolicy = - getHashAlgorithmPolicyForSignature(signature, policy); - if (!hashAlgorithmPolicy.isAcceptable(signature.getHashAlgorithm(), signature.getCreationTime())) { - throw new SignatureValidationException("Signature uses unacceptable hash algorithm " + - hashAlgorithm + " (Signature creation time: " + - DateUtil.formatUTCDate(signature.getCreationTime()) + ")"); - } - } catch (NoSuchElementException e) { - throw new SignatureValidationException("Signature uses unknown hash algorithm " + - signature.getHashAlgorithm()); - } - } - }; - } - - /** - * 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) { - SignatureType type = SignatureType.valueOf(signature.getSignatureType()); - Policy.HashAlgorithmPolicy hashAlgorithmPolicy; - 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.requireFromCode(criticalTag); - } catch (NoSuchElementException 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 referenceDate reference date for signature verification - * @return validator - */ - public static SignatureValidator signatureIsEffective(Date referenceDate) { - return new SignatureValidator() { - @Override - public void verify(PGPSignature signature) throws SignatureValidationException { - signatureIsAlreadyEffective(referenceDate).verify(signature); - signatureIsNotYetExpired(referenceDate).verify(signature); - } - }; - } - - /** - * Verify that a signature was created prior to the given reference date. - * - * @param referenceDate reference date for signature verification - * @return validator - */ - public static SignatureValidator signatureIsAlreadyEffective(Date referenceDate) { - 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(referenceDate)) { - throw new SignatureValidationException("Signature was created at " + signatureCreationTime + - " and is therefore not yet valid at " + referenceDate); - } - } - }; - } - - /** - * Verify that a signature is not yet expired. - * - * @param referenceDate reference date for signature verification - * @return validator - */ - public static SignatureValidator signatureIsNotYetExpired(Date referenceDate) { - 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(referenceDate)) { - throw new SignatureValidationException("Signature is already expired (expiration: " + - signatureExpirationTime + ", validation: " + referenceDate + ")"); - } - } - }; - } - - /** - * 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 { - if (signature.getVersion() >= 4) { - signatureHasHashedCreationTime().verify(signature); - } - signatureDoesNotPredateSigningKey(creator).verify(signature); - if (signature.getSignatureType() != SignatureType.PRIMARYKEY_BINDING.getCode()) { - signatureDoesNotPredateSigningKeyBindingDate(creator).verify(signature); - } - } - }; - } - - public static SignatureValidator signatureDoesNotPredateSignee(PGPPublicKey signee) { - return signatureDoesNotPredateKeyCreation(signee); - } - - /** - * 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 signatureDoesNotPredateKeyCreation(key); - } - - /** - * Verify that a signature does not predate the creation time of the given key. - * - * @param key key - * @return validator - */ - public static SignatureValidator signatureDoesNotPredateKeyCreation(PGPPublicKey key) { - return new SignatureValidator() { - @Override - public void verify(PGPSignature signature) throws SignatureValidationException { - Date keyCreationTime = key.getCreationTime(); - Date signatureCreationTime = signature.getCreationTime(); - - if (keyCreationTime.after(signatureCreationTime)) { - throw new SignatureValidationException("Signature predates 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 | ClassCastException 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 | ClassCastException 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; - 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 | ClassCastException 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 | ClassCastException 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 | ClassCastException 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/consumer/SignatureValidityComparator.java b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureValidityComparator.java deleted file mode 100644 index 94ebd5b2..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureValidityComparator.java +++ /dev/null @@ -1,53 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature.consumer; - -import java.util.Comparator; - -import org.bouncycastle.openpgp.PGPSignature; -import org.pgpainless.signature.SignatureUtils; - -/** - * 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 SignatureCreationDateComparator.Order}. - */ -public class SignatureValidityComparator implements Comparator { - - private final SignatureCreationDateComparator creationDateComparator; - - /** - * Create a new {@link SignatureValidityComparator} which orders signatures the 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. - * - * @param order order of creation dates - */ - 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/consumer/SignatureVerifier.java b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureVerifier.java deleted file mode 100644 index c4565197..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureVerifier.java +++ /dev/null @@ -1,486 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature.consumer; - -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 referenceDate 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 referenceDate) - 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, referenceDate); - case CERTIFICATION_REVOCATION: - return verifyUserIdRevocation(userId, signature, signingKey, keyWithUserId, policy, referenceDate); - 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 referenceDate 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 referenceDate) - throws SignatureValidationException { - return verifyUserIdCertification(userId, signature, primaryKey, primaryKey, policy, referenceDate); - } - - /** - * 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 referenceDate 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 referenceDate) - throws SignatureValidationException { - SignatureValidator.wasPossiblyMadeByKey(signingKey).verify(signature); - SignatureValidator.signatureIsCertification().verify(signature); - SignatureValidator.signatureStructureIsAcceptable(signingKey, policy).verify(signature); - SignatureValidator.signatureIsEffective(referenceDate).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 referenceDate 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 referenceDate) - throws SignatureValidationException { - return verifyUserIdRevocation(userId, signature, primaryKey, primaryKey, policy, referenceDate); - } - - /** - * 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 referenceDate 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 referenceDate) - throws SignatureValidationException { - SignatureValidator.wasPossiblyMadeByKey(signingKey).verify(signature); - SignatureValidator.signatureIsOfType(SignatureType.CERTIFICATION_REVOCATION).verify(signature); - SignatureValidator.signatureStructureIsAcceptable(signingKey, policy).verify(signature); - SignatureValidator.signatureIsEffective(referenceDate).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 referenceDate 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 referenceDate) - throws SignatureValidationException { - return verifyUserAttributesCertification(userAttributes, signature, primaryKey, primaryKey, policy, - referenceDate); - } - - /** - * 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 referenceDate 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 referenceDate) - throws SignatureValidationException { - SignatureValidator.wasPossiblyMadeByKey(signingKey).verify(signature); - SignatureValidator.signatureIsCertification().verify(signature); - SignatureValidator.signatureStructureIsAcceptable(signingKey, policy).verify(signature); - SignatureValidator.signatureIsEffective(referenceDate).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 referenceDate 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 referenceDate) - throws SignatureValidationException { - return verifyUserAttributesRevocation(userAttributes, signature, primaryKey, primaryKey, policy, referenceDate); - } - - /** - * 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 referenceDate 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 referenceDate) - throws SignatureValidationException { - SignatureValidator.wasPossiblyMadeByKey(signingKey).verify(signature); - SignatureValidator.signatureIsOfType(SignatureType.CERTIFICATION_REVOCATION).verify(signature); - SignatureValidator.signatureStructureIsAcceptable(signingKey, policy).verify(signature); - SignatureValidator.signatureIsEffective(referenceDate).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 referenceDate 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 referenceDate) - throws SignatureValidationException { - SignatureValidator.signatureIsOfType(SignatureType.SUBKEY_BINDING).verify(signature); - SignatureValidator.signatureStructureIsAcceptable(primaryKey, policy).verify(signature); - SignatureValidator.signatureDoesNotPredateSignee(subkey).verify(signature); - SignatureValidator.signatureIsEffective(referenceDate).verify(signature); - SignatureValidator.hasValidPrimaryKeyBindingSignatureIfRequired(primaryKey, subkey, policy, referenceDate) - .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 referenceDate 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 referenceDate) - throws SignatureValidationException { - SignatureValidator.signatureIsOfType(SignatureType.SUBKEY_REVOCATION).verify(signature); - SignatureValidator.signatureStructureIsAcceptable(primaryKey, policy).verify(signature); - SignatureValidator.signatureDoesNotPredateSignee(subkey).verify(signature); - SignatureValidator.signatureIsEffective(referenceDate).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 referenceDate 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 referenceDate) - throws SignatureValidationException { - return verifyDirectKeySignature(signature, primaryKey, primaryKey, policy, referenceDate); - } - - /** - * Verify a direct-key signature. - * - * @param signature signature - * @param signingKey signing key - * @param signedKey signed key - * @param policy policy - * @param referenceDate 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 referenceDate) - throws SignatureValidationException { - SignatureValidator.signatureIsOfType(SignatureType.DIRECT_KEY).verify(signature); - SignatureValidator.signatureStructureIsAcceptable(signingKey, policy).verify(signature); - SignatureValidator.signatureDoesNotPredateSignee(signedKey).verify(signature); - SignatureValidator.signatureIsEffective(referenceDate).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 referenceDate 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 referenceDate) - throws SignatureValidationException { - SignatureValidator.signatureIsOfType(SignatureType.KEY_REVOCATION).verify(signature); - SignatureValidator.signatureStructureIsAcceptable(primaryKey, policy).verify(signature); - SignatureValidator.signatureIsEffective(referenceDate).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 referenceDate 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 referenceDate) - throws SignatureValidationException { - initializeSignatureAndUpdateWithSignedData(signature, signedData, signingKey); - return verifyInitializedSignature(signature, signingKey, policy, referenceDate); - } - - /** - * 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 referenceDate 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 referenceDate) - throws SignatureValidationException { - SignatureValidator.wasPossiblyMadeByKey(signingKey).verify(signature); - SignatureValidator.signatureStructureIsAcceptable(signingKey, policy).verify(signature); - SignatureValidator.signatureIsEffective(referenceDate).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 referenceDate 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 referenceDate) - throws SignatureValidationException { - return verifySignatureOverUserId(userId, signature, primaryKey, primaryKey, policy, referenceDate); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/package-info.java deleted file mode 100644 index e8e53285..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Classes related to OpenPGP signature verification. - */ -package org.pgpainless.signature.consumer; 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/BaseSignatureSubpackets.java b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/BaseSignatureSubpackets.java deleted file mode 100644 index 28e99b9e..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/BaseSignatureSubpackets.java +++ /dev/null @@ -1,131 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature.subpackets; - -import java.io.IOException; -import java.net.URL; -import java.util.Date; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import org.bouncycastle.bcpg.sig.EmbeddedSignature; -import org.bouncycastle.bcpg.sig.Exportable; -import org.bouncycastle.bcpg.sig.IntendedRecipientFingerprint; -import org.bouncycastle.bcpg.sig.IssuerFingerprint; -import org.bouncycastle.bcpg.sig.IssuerKeyID; -import org.bouncycastle.bcpg.sig.NotationData; -import org.bouncycastle.bcpg.sig.PolicyURI; -import org.bouncycastle.bcpg.sig.RegularExpression; -import org.bouncycastle.bcpg.sig.Revocable; -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.PGPPublicKey; -import org.bouncycastle.openpgp.PGPSignature; -import org.pgpainless.algorithm.HashAlgorithm; -import org.pgpainless.algorithm.PublicKeyAlgorithm; - -public interface BaseSignatureSubpackets { - - interface Callback extends SignatureSubpacketCallback { - - } - - BaseSignatureSubpackets setIssuerFingerprintAndKeyId(PGPPublicKey key); - - BaseSignatureSubpackets setIssuerKeyId(long keyId); - - BaseSignatureSubpackets setIssuerKeyId(boolean isCritical, long keyId); - - BaseSignatureSubpackets setIssuerKeyId(@Nullable IssuerKeyID issuerKeyID); - - BaseSignatureSubpackets setIssuerFingerprint(@Nonnull PGPPublicKey key); - - BaseSignatureSubpackets setIssuerFingerprint(boolean isCritical, @Nonnull PGPPublicKey key); - - BaseSignatureSubpackets setIssuerFingerprint(@Nullable IssuerFingerprint fingerprint); - - BaseSignatureSubpackets setSignatureCreationTime(@Nonnull Date creationTime); - - BaseSignatureSubpackets setSignatureCreationTime(boolean isCritical, @Nonnull Date creationTime); - - BaseSignatureSubpackets setSignatureCreationTime(@Nullable SignatureCreationTime signatureCreationTime); - - BaseSignatureSubpackets setSignatureExpirationTime(@Nonnull Date creationTime, @Nonnull Date expirationTime); - - BaseSignatureSubpackets setSignatureExpirationTime(boolean isCritical, @Nonnull Date creationTime, @Nonnull Date expirationTime); - - BaseSignatureSubpackets setSignatureExpirationTime(boolean isCritical, long seconds); - - BaseSignatureSubpackets setSignatureExpirationTime(@Nullable SignatureExpirationTime expirationTime); - - BaseSignatureSubpackets setSignerUserId(@Nonnull String userId); - - BaseSignatureSubpackets setSignerUserId(boolean isCritical, @Nonnull String userId); - - BaseSignatureSubpackets setSignerUserId(@Nullable SignerUserID signerUserId); - - BaseSignatureSubpackets addNotationData(boolean isCritical, @Nonnull String notationName, @Nonnull String notationValue); - - BaseSignatureSubpackets addNotationData(boolean isCritical, boolean isHumanReadable, @Nonnull String notationName, @Nonnull String notationValue); - - BaseSignatureSubpackets addNotationData(@Nonnull NotationData notationData); - - BaseSignatureSubpackets clearNotationData(); - - BaseSignatureSubpackets addIntendedRecipientFingerprint(@Nonnull PGPPublicKey recipient); - - BaseSignatureSubpackets addIntendedRecipientFingerprint(boolean isCritical, @Nonnull PGPPublicKey recipient); - - BaseSignatureSubpackets addIntendedRecipientFingerprint(IntendedRecipientFingerprint intendedRecipientFingerprint); - - BaseSignatureSubpackets clearIntendedRecipientFingerprints(); - - BaseSignatureSubpackets setExportable(boolean isExportable); - - BaseSignatureSubpackets setExportable(boolean isCritical, boolean isExportable); - - BaseSignatureSubpackets setExportable(@Nullable Exportable exportable); - - BaseSignatureSubpackets setPolicyUrl(@Nonnull URL policyUrl); - - BaseSignatureSubpackets setPolicyUrl(boolean isCritical, @Nonnull URL policyUrl); - - BaseSignatureSubpackets setPolicyUrl(@Nullable PolicyURI policyUrl); - - BaseSignatureSubpackets setRegularExpression(@Nonnull String regex); - - BaseSignatureSubpackets setRegularExpression(boolean isCritical, @Nonnull String regex); - - BaseSignatureSubpackets setRegularExpression(@Nullable RegularExpression regex); - - BaseSignatureSubpackets setRevocable(boolean revocable); - - BaseSignatureSubpackets setRevocable(boolean isCritical, boolean isRevocable); - - BaseSignatureSubpackets setRevocable(@Nullable Revocable revocable); - - BaseSignatureSubpackets setSignatureTarget(@Nonnull PublicKeyAlgorithm keyAlgorithm, @Nonnull HashAlgorithm hashAlgorithm, @Nonnull byte[] hashData); - - BaseSignatureSubpackets setSignatureTarget(boolean isCritical, @Nonnull PublicKeyAlgorithm keyAlgorithm, @Nonnull HashAlgorithm hashAlgorithm, @Nonnull byte[] hashData); - - BaseSignatureSubpackets setSignatureTarget(@Nullable SignatureTarget signatureTarget); - - BaseSignatureSubpackets setTrust(int depth, int amount); - - BaseSignatureSubpackets setTrust(boolean isCritical, int depth, int amount); - - BaseSignatureSubpackets setTrust(@Nullable TrustSignature trust); - - BaseSignatureSubpackets addEmbeddedSignature(@Nonnull PGPSignature signature) throws IOException; - - BaseSignatureSubpackets addEmbeddedSignature(boolean isCritical, @Nonnull PGPSignature signature) throws IOException; - - BaseSignatureSubpackets addEmbeddedSignature(@Nonnull EmbeddedSignature embeddedSignature); - - BaseSignatureSubpackets clearEmbeddedSignatures(); -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/CertificationSubpackets.java b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/CertificationSubpackets.java deleted file mode 100644 index 24614882..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/CertificationSubpackets.java +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature.subpackets; - -public interface CertificationSubpackets extends BaseSignatureSubpackets { - - interface Callback extends SignatureSubpacketCallback { - - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/RevocationSignatureSubpackets.java b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/RevocationSignatureSubpackets.java deleted file mode 100644 index 358437dc..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/RevocationSignatureSubpackets.java +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature.subpackets; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import org.bouncycastle.bcpg.sig.RevocationReason; -import org.pgpainless.key.util.RevocationAttributes; - -public interface RevocationSignatureSubpackets extends BaseSignatureSubpackets { - - interface Callback extends SignatureSubpacketCallback { - - } - - RevocationSignatureSubpackets setRevocationReason(RevocationAttributes revocationAttributes); - - RevocationSignatureSubpackets setRevocationReason(boolean isCritical, RevocationAttributes revocationAttributes); - - RevocationSignatureSubpackets setRevocationReason(boolean isCritical, RevocationAttributes.Reason reason, @Nonnull String description); - - RevocationSignatureSubpackets setRevocationReason(@Nullable RevocationReason reason); -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SelfSignatureSubpackets.java b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SelfSignatureSubpackets.java deleted file mode 100644 index 02cc5e93..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SelfSignatureSubpackets.java +++ /dev/null @@ -1,98 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature.subpackets; - -import java.util.Date; -import java.util.List; -import java.util.Set; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -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.CompressionAlgorithm; -import org.pgpainless.algorithm.Feature; -import org.pgpainless.algorithm.HashAlgorithm; -import org.pgpainless.algorithm.KeyFlag; -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; - -public interface SelfSignatureSubpackets extends BaseSignatureSubpackets { - - interface Callback extends SignatureSubpacketCallback { - - } - - SelfSignatureSubpackets setKeyFlags(KeyFlag... keyFlags); - - default SelfSignatureSubpackets setKeyFlags(List keyFlags) { - KeyFlag[] flags = keyFlags.toArray(new KeyFlag[0]); - return setKeyFlags(flags); - } - - SelfSignatureSubpackets setKeyFlags(boolean isCritical, KeyFlag... keyFlags); - - SelfSignatureSubpackets setKeyFlags(@Nullable KeyFlags keyFlags); - - SelfSignatureSubpackets setPrimaryUserId(); - - SelfSignatureSubpackets setPrimaryUserId(boolean isCritical); - - SelfSignatureSubpackets setPrimaryUserId(@Nullable PrimaryUserID primaryUserId); - - SelfSignatureSubpackets setKeyExpirationTime(@Nonnull PGPPublicKey key, @Nonnull Date keyExpirationTime); - - SelfSignatureSubpackets setKeyExpirationTime(@Nonnull Date keyCreationTime, @Nonnull Date keyExpirationTime); - - SelfSignatureSubpackets setKeyExpirationTime(boolean isCritical, @Nonnull Date keyCreationTime, @Nonnull Date keyExpirationTime); - - SelfSignatureSubpackets setKeyExpirationTime(boolean isCritical, long secondsFromCreationToExpiration); - - SelfSignatureSubpackets setKeyExpirationTime(@Nullable KeyExpirationTime keyExpirationTime); - - SelfSignatureSubpackets setPreferredCompressionAlgorithms(CompressionAlgorithm... algorithms); - - SelfSignatureSubpackets setPreferredCompressionAlgorithms(Set algorithms); - - SelfSignatureSubpackets setPreferredCompressionAlgorithms(boolean isCritical, Set algorithms); - - SelfSignatureSubpackets setPreferredCompressionAlgorithms(@Nullable PreferredAlgorithms algorithms); - - SelfSignatureSubpackets setPreferredSymmetricKeyAlgorithms(SymmetricKeyAlgorithm... algorithms); - - SelfSignatureSubpackets setPreferredSymmetricKeyAlgorithms(Set algorithms); - - SelfSignatureSubpackets setPreferredSymmetricKeyAlgorithms(boolean isCritical, Set algorithms); - - SelfSignatureSubpackets setPreferredSymmetricKeyAlgorithms(@Nullable PreferredAlgorithms algorithms); - - SelfSignatureSubpackets setPreferredHashAlgorithms(HashAlgorithm... algorithms); - - SelfSignatureSubpackets setPreferredHashAlgorithms(Set algorithms); - - SelfSignatureSubpackets setPreferredHashAlgorithms(boolean isCritical, Set algorithms); - - SelfSignatureSubpackets setPreferredHashAlgorithms(@Nullable PreferredAlgorithms algorithms); - - SelfSignatureSubpackets addRevocationKey(@Nonnull PGPPublicKey revocationKey); - - SelfSignatureSubpackets addRevocationKey(boolean isCritical, @Nonnull PGPPublicKey revocationKey); - - SelfSignatureSubpackets addRevocationKey(boolean isCritical, boolean isSensitive, @Nonnull PGPPublicKey revocationKey); - - SelfSignatureSubpackets addRevocationKey(@Nonnull RevocationKey revocationKey); - - SelfSignatureSubpackets clearRevocationKeys(); - - SelfSignatureSubpackets setFeatures(Feature... features); - - SelfSignatureSubpackets setFeatures(boolean isCritical, Feature... features); - - SelfSignatureSubpackets setFeatures(@Nullable Features features); -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketCallback.java b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketCallback.java deleted file mode 100644 index 11650ea3..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketCallback.java +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature.subpackets; - -public interface SignatureSubpacketCallback { - - /** - * Callback method that can be used to modify the hashed subpackets of a signature. - * - * @param hashedSubpackets hashed subpackets - */ - default void modifyHashedSubpackets(S hashedSubpackets) { - - } - - /** - * Callback method that can be used to modify the unhashed subpackets of a signature. - * - * @param unhashedSubpackets unhashed subpackets - */ - default void modifyUnhashedSubpackets(S unhashedSubpackets) { - - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpackets.java b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpackets.java deleted file mode 100644 index d0466b6a..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpackets.java +++ /dev/null @@ -1,735 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature.subpackets; - -import java.io.IOException; -import java.net.URL; -import java.util.ArrayList; -import java.util.Arrays; -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 javax.annotation.Nullable; - -import org.bouncycastle.bcpg.SignatureSubpacket; -import org.bouncycastle.bcpg.SignatureSubpacketTags; -import org.bouncycastle.bcpg.sig.EmbeddedSignature; -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.PolicyURI; -import org.bouncycastle.bcpg.sig.PreferredAlgorithms; -import org.bouncycastle.bcpg.sig.PrimaryUserID; -import org.bouncycastle.bcpg.sig.RegularExpression; -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.PGPPublicKey; -import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.PGPSignatureSubpacketVector; -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.SymmetricKeyAlgorithm; -import org.pgpainless.key.util.RevocationAttributes; - -public class SignatureSubpackets - implements BaseSignatureSubpackets, SelfSignatureSubpackets, CertificationSubpackets, RevocationSignatureSubpackets { - - private SignatureCreationTime signatureCreationTime; - private SignatureExpirationTime signatureExpirationTime; - private IssuerKeyID issuerKeyID; - private IssuerFingerprint issuerFingerprint; - private final List notationDataList = new ArrayList<>(); - private final List intendedRecipientFingerprintList = new ArrayList<>(); - private final List revocationKeyList = new ArrayList<>(); - private Exportable exportable; - private SignatureTarget signatureTarget; - private Features features; - private KeyFlags keyFlags; - private TrustSignature trust; - private PreferredAlgorithms preferredCompressionAlgorithms; - private PreferredAlgorithms preferredSymmetricKeyAlgorithms; - private PreferredAlgorithms preferredHashAlgorithms; - private final List embeddedSignatureList = new ArrayList<>(); - private SignerUserID signerUserId; - private KeyExpirationTime keyExpirationTime; - private PolicyURI policyURI; - private PrimaryUserID primaryUserId; - private RegularExpression regularExpression; - private Revocable revocable; - private RevocationReason revocationReason; - private final List residualSubpackets = new ArrayList<>(); - - public SignatureSubpackets() { - - } - - public interface Callback extends SignatureSubpacketCallback { - - } - - public static SignatureSubpackets refreshHashedSubpackets(PGPPublicKey issuer, PGPSignature oldSignature) { - return createHashedSubpacketsFrom(issuer, oldSignature.getHashedSubPackets()); - } - - public static SignatureSubpackets refreshUnhashedSubpackets(PGPSignature oldSignature) { - return createSubpacketsFrom(oldSignature.getUnhashedSubPackets()); - } - - public static SignatureSubpackets createHashedSubpacketsFrom(PGPPublicKey issuer, PGPSignatureSubpacketVector base) { - SignatureSubpackets wrapper = createSubpacketsFrom(base); - wrapper.setIssuerFingerprintAndKeyId(issuer); - return wrapper; - } - - public static SignatureSubpackets createSubpacketsFrom(PGPSignatureSubpacketVector base) { - SignatureSubpackets wrapper = new SignatureSubpackets(); - SignatureSubpacketsHelper.applyFrom(base, wrapper); - return wrapper; - } - - public static SignatureSubpackets createHashedSubpackets(PGPPublicKey issuer) { - SignatureSubpackets wrapper = new SignatureSubpackets(); - wrapper.setIssuerFingerprintAndKeyId(issuer); - return wrapper; - } - - public static SignatureSubpackets createEmptySubpackets() { - return new SignatureSubpackets(); - } - - @Override - public SignatureSubpackets setIssuerFingerprintAndKeyId(PGPPublicKey key) { - setIssuerKeyId(key.getKeyID()); - setIssuerFingerprint(key); - return this; - } - - @Override - public SignatureSubpackets setIssuerKeyId(long keyId) { - return setIssuerKeyId(false, keyId); - } - - @Override - public SignatureSubpackets setIssuerKeyId(boolean isCritical, long keyId) { - return setIssuerKeyId(new IssuerKeyID(isCritical, keyId)); - } - - @Override - public SignatureSubpackets setIssuerKeyId(@Nullable IssuerKeyID issuerKeyID) { - this.issuerKeyID = issuerKeyID; - return this; - } - - public IssuerKeyID getIssuerKeyIdSubpacket() { - return issuerKeyID; - } - - @Override - public SignatureSubpackets setIssuerFingerprint(@Nonnull PGPPublicKey key) { - return setIssuerFingerprint(false, key); - } - - @Override - public SignatureSubpackets setIssuerFingerprint(boolean isCritical, @Nonnull PGPPublicKey key) { - return setIssuerFingerprint(new IssuerFingerprint(isCritical, key.getVersion(), key.getFingerprint())); - } - - @Override - public SignatureSubpackets setIssuerFingerprint(@Nullable IssuerFingerprint fingerprint) { - this.issuerFingerprint = fingerprint; - return this; - } - - public IssuerFingerprint getIssuerFingerprintSubpacket() { - return issuerFingerprint; - } - - @Override - public SignatureSubpackets setKeyFlags(KeyFlag... keyFlags) { - return setKeyFlags(true, keyFlags); - } - - @Override - public SignatureSubpackets setKeyFlags(boolean isCritical, KeyFlag... keyFlags) { - int bitmask = KeyFlag.toBitmask(keyFlags); - return setKeyFlags(new KeyFlags(isCritical, bitmask)); - } - - @Override - public SignatureSubpackets setKeyFlags(@Nullable KeyFlags keyFlags) { - this.keyFlags = keyFlags; - return this; - } - - public KeyFlags getKeyFlagsSubpacket() { - return keyFlags; - } - - @Override - public SignatureSubpackets setSignatureCreationTime(@Nonnull Date creationTime) { - return setSignatureCreationTime(true, creationTime); - } - - @Override - public SignatureSubpackets setSignatureCreationTime(boolean isCritical, @Nonnull Date creationTime) { - return setSignatureCreationTime(new SignatureCreationTime(isCritical, creationTime)); - } - - @Override - public SignatureSubpackets setSignatureCreationTime(@Nullable SignatureCreationTime signatureCreationTime) { - this.signatureCreationTime = signatureCreationTime; - return this; - } - - public SignatureCreationTime getSignatureCreationTimeSubpacket() { - return signatureCreationTime; - } - - @Override - public SignatureSubpackets setSignatureExpirationTime(@Nonnull Date creationTime, @Nonnull Date expirationTime) { - return setSignatureExpirationTime(true, creationTime, expirationTime); - } - - @Override - public SignatureSubpackets setSignatureExpirationTime(boolean isCritical, @Nonnull Date creationTime, @Nonnull Date expirationTime) { - return setSignatureExpirationTime(isCritical, (expirationTime.getTime() / 1000) - (creationTime.getTime() / 1000)); - } - - @Override - public SignatureSubpackets setSignatureExpirationTime(boolean isCritical, long seconds) { - enforceBounds(seconds); - return setSignatureExpirationTime(new SignatureExpirationTime(isCritical, seconds)); - } - - @Override - public SignatureSubpackets setSignatureExpirationTime(@Nullable SignatureExpirationTime expirationTime) { - this.signatureExpirationTime = expirationTime; - return this; - } - - public SignatureExpirationTime getSignatureExpirationTimeSubpacket() { - return signatureExpirationTime; - } - - @Override - public SignatureSubpackets setSignerUserId(@Nonnull String userId) { - return setSignerUserId(false, userId); - } - - @Override - public SignatureSubpackets setSignerUserId(boolean isCritical, @Nonnull String userId) { - return setSignerUserId(new SignerUserID(isCritical, userId)); - } - - @Override - public SignatureSubpackets setSignerUserId(@Nullable SignerUserID signerUserId) { - this.signerUserId = signerUserId; - return this; - } - - public SignerUserID getSignerUserIdSubpacket() { - return signerUserId; - } - - @Override - public SignatureSubpackets setPrimaryUserId() { - return setPrimaryUserId(true); - } - - @Override - public SignatureSubpackets setPrimaryUserId(boolean isCritical) { - return setPrimaryUserId(new PrimaryUserID(isCritical, true)); - } - - @Override - public SignatureSubpackets setPrimaryUserId(@Nullable PrimaryUserID primaryUserId) { - this.primaryUserId = primaryUserId; - return this; - } - - public PrimaryUserID getPrimaryUserIdSubpacket() { - return primaryUserId; - } - - @Override - public SignatureSubpackets setKeyExpirationTime(@Nonnull PGPPublicKey key, @Nonnull Date keyExpirationTime) { - return setKeyExpirationTime(key.getCreationTime(), keyExpirationTime); - } - - @Override - public SignatureSubpackets setKeyExpirationTime(@Nonnull Date keyCreationTime, @Nonnull Date keyExpirationTime) { - return setKeyExpirationTime(true, keyCreationTime, keyExpirationTime); - } - - @Override - public SignatureSubpackets setKeyExpirationTime(boolean isCritical, @Nonnull Date keyCreationTime, @Nonnull Date keyExpirationTime) { - return setKeyExpirationTime(isCritical, (keyExpirationTime.getTime() / 1000) - (keyCreationTime.getTime() / 1000)); - } - - @Override - public SignatureSubpackets setKeyExpirationTime(boolean isCritical, long secondsFromCreationToExpiration) { - enforceBounds(secondsFromCreationToExpiration); - return setKeyExpirationTime(new KeyExpirationTime(isCritical, secondsFromCreationToExpiration)); - } - - /** - * Enforce that
secondsFromCreationToExpiration
is within bounds of an unsigned 32bit number. - * Values less than 0 are illegal, as well as values greater 0xffffffff. - * - * @param secondsFromCreationToExpiration number to check - * @throws IllegalArgumentException in case of an under- or overflow - */ - private void enforceBounds(long secondsFromCreationToExpiration) { - if (secondsFromCreationToExpiration < 0) { - throw new IllegalArgumentException("Seconds from creation to expiration cannot be less than 0."); - } - if (secondsFromCreationToExpiration > 0xffffffffL) { - throw new IllegalArgumentException("Integer overflow. Seconds from creation to expiration cannot be larger than 0xffffffff"); - } - } - - @Override - public SignatureSubpackets setKeyExpirationTime(@Nullable KeyExpirationTime keyExpirationTime) { - this.keyExpirationTime = keyExpirationTime; - return this; - } - - public KeyExpirationTime getKeyExpirationTimeSubpacket() { - return keyExpirationTime; - } - - @Override - public SignatureSubpackets setPreferredCompressionAlgorithms(CompressionAlgorithm... algorithms) { - return setPreferredCompressionAlgorithms(new LinkedHashSet<>(Arrays.asList(algorithms))); - } - - @Override - public SignatureSubpackets setPreferredCompressionAlgorithms(Set algorithms) { - return setPreferredCompressionAlgorithms(false, algorithms); - } - - @Override - public SignatureSubpackets setPreferredCompressionAlgorithms(boolean isCritical, Set algorithms) { - int[] ids = new int[algorithms.size()]; - Iterator iterator = algorithms.iterator(); - for (int i = 0; i < algorithms.size(); i++) { - ids[i] = iterator.next().getAlgorithmId(); - } - return setPreferredCompressionAlgorithms(new PreferredAlgorithms( - SignatureSubpacketTags.PREFERRED_COMP_ALGS, isCritical, ids)); - } - - @Override - public SignatureSubpackets setPreferredCompressionAlgorithms(@Nullable PreferredAlgorithms algorithms) { - if (algorithms == null) { - this.preferredCompressionAlgorithms = null; - return this; - } - - if (algorithms.getType() != SignatureSubpacketTags.PREFERRED_COMP_ALGS) { - throw new IllegalArgumentException("Invalid preferred compression algorithms type."); - } - this.preferredCompressionAlgorithms = algorithms; - return this; - } - - public PreferredAlgorithms getPreferredCompressionAlgorithmsSubpacket() { - return preferredCompressionAlgorithms; - } - - @Override - public SignatureSubpackets setPreferredSymmetricKeyAlgorithms(SymmetricKeyAlgorithm... algorithms) { - return setPreferredSymmetricKeyAlgorithms(new LinkedHashSet<>(Arrays.asList(algorithms))); - } - - @Override - public SignatureSubpackets setPreferredSymmetricKeyAlgorithms(Set algorithms) { - return setPreferredSymmetricKeyAlgorithms(false, algorithms); - } - - @Override - public SignatureSubpackets setPreferredSymmetricKeyAlgorithms(boolean isCritical, Set algorithms) { - int[] ids = new int[algorithms.size()]; - Iterator iterator = algorithms.iterator(); - for (int i = 0; i < algorithms.size(); i++) { - ids[i] = iterator.next().getAlgorithmId(); - } - return setPreferredSymmetricKeyAlgorithms(new PreferredAlgorithms( - SignatureSubpacketTags.PREFERRED_SYM_ALGS, isCritical, ids)); - } - - @Override - public SignatureSubpackets setPreferredSymmetricKeyAlgorithms(@Nullable PreferredAlgorithms algorithms) { - if (algorithms == null) { - this.preferredSymmetricKeyAlgorithms = null; - return this; - } - - if (algorithms.getType() != SignatureSubpacketTags.PREFERRED_SYM_ALGS) { - throw new IllegalArgumentException("Invalid preferred symmetric key algorithms type."); - } - this.preferredSymmetricKeyAlgorithms = algorithms; - return this; - } - - public PreferredAlgorithms getPreferredSymmetricKeyAlgorithmsSubpacket() { - return preferredSymmetricKeyAlgorithms; - } - - @Override - public SignatureSubpackets setPreferredHashAlgorithms(HashAlgorithm... algorithms) { - return setPreferredHashAlgorithms(new LinkedHashSet<>(Arrays.asList(algorithms))); - } - - @Override - public SignatureSubpackets setPreferredHashAlgorithms(Set algorithms) { - return setPreferredHashAlgorithms(false, algorithms); - } - - @Override - public SignatureSubpackets setPreferredHashAlgorithms(boolean isCritical, Set algorithms) { - int[] ids = new int[algorithms.size()]; - Iterator iterator = algorithms.iterator(); - for (int i = 0; i < ids.length; i++) { - ids[i] = iterator.next().getAlgorithmId(); - } - return setPreferredHashAlgorithms(new PreferredAlgorithms( - SignatureSubpacketTags.PREFERRED_HASH_ALGS, isCritical, ids)); - } - - @Override - public SignatureSubpackets setPreferredHashAlgorithms(@Nullable PreferredAlgorithms algorithms) { - if (algorithms == null) { - preferredHashAlgorithms = null; - return this; - } - - if (algorithms.getType() != SignatureSubpacketTags.PREFERRED_HASH_ALGS) { - throw new IllegalArgumentException("Invalid preferred hash algorithms type."); - } - this.preferredHashAlgorithms = algorithms; - return this; - } - - public PreferredAlgorithms getPreferredHashAlgorithmsSubpacket() { - return preferredHashAlgorithms; - } - - @Override - public SignatureSubpackets addNotationData(boolean isCritical, @Nonnull String notationName, @Nonnull String notationValue) { - return addNotationData(isCritical, true, notationName, notationValue); - } - - @Override - public SignatureSubpackets addNotationData(boolean isCritical, boolean isHumanReadable, @Nonnull String notationName, @Nonnull String notationValue) { - return addNotationData(new NotationData(isCritical, isHumanReadable, notationName, notationValue)); - } - - @Override - public SignatureSubpackets addNotationData(@Nonnull NotationData notationData) { - notationDataList.add(notationData); - return this; - } - - @Override - public SignatureSubpackets clearNotationData() { - notationDataList.clear(); - return this; - } - - public List getNotationDataSubpackets() { - return new ArrayList<>(notationDataList); - } - - @Override - public SignatureSubpackets addIntendedRecipientFingerprint(@Nonnull PGPPublicKey recipient) { - return addIntendedRecipientFingerprint(false, recipient); - } - - @Override - public SignatureSubpackets addIntendedRecipientFingerprint(boolean isCritical, @Nonnull PGPPublicKey recipient) { - return addIntendedRecipientFingerprint(new IntendedRecipientFingerprint(isCritical, recipient.getVersion(), recipient.getFingerprint())); - } - - @Override - public SignatureSubpackets addIntendedRecipientFingerprint(IntendedRecipientFingerprint intendedRecipientFingerprint) { - this.intendedRecipientFingerprintList.add(intendedRecipientFingerprint); - return this; - } - - @Override - public SignatureSubpackets clearIntendedRecipientFingerprints() { - intendedRecipientFingerprintList.clear(); - return this; - } - - public List getIntendedRecipientFingerprintSubpackets() { - return new ArrayList<>(intendedRecipientFingerprintList); - } - - @Override - public SignatureSubpackets setExportable(boolean exportable) { - return setExportable(true, exportable); - } - - @Override - public SignatureSubpackets setExportable(boolean isCritical, boolean isExportable) { - return setExportable(new Exportable(isCritical, isExportable)); - } - - @Override - public SignatureSubpackets setExportable(@Nullable Exportable exportable) { - this.exportable = exportable; - return this; - } - - public Exportable getExportableSubpacket() { - return exportable; - } - - @Override - public BaseSignatureSubpackets setPolicyUrl(@Nonnull URL policyUrl) { - return setPolicyUrl(false, policyUrl); - } - - @Override - public BaseSignatureSubpackets setPolicyUrl(boolean isCritical, @Nonnull URL policyUrl) { - return setPolicyUrl(new PolicyURI(isCritical, policyUrl.toString())); - } - - @Override - public BaseSignatureSubpackets setPolicyUrl(@Nullable PolicyURI policyUrl) { - this.policyURI = policyUrl; - return this; - } - - public PolicyURI getPolicyURI() { - return policyURI; - } - - @Override - public BaseSignatureSubpackets setRegularExpression(@Nonnull String regex) { - return setRegularExpression(false, regex); - } - - @Override - public BaseSignatureSubpackets setRegularExpression(boolean isCritical, @Nonnull String regex) { - return setRegularExpression(new RegularExpression(isCritical, regex)); - } - - @Override - public BaseSignatureSubpackets setRegularExpression(@Nullable RegularExpression regex) { - this.regularExpression = regex; - return this; - } - - public RegularExpression getRegularExpression() { - return regularExpression; - } - - @Override - public SignatureSubpackets setRevocable(boolean revocable) { - return setRevocable(true, revocable); - } - - @Override - public SignatureSubpackets setRevocable(boolean isCritical, boolean isRevocable) { - return setRevocable(new Revocable(isCritical, isRevocable)); - } - - @Override - public SignatureSubpackets setRevocable(@Nullable Revocable revocable) { - this.revocable = revocable; - return this; - } - - public Revocable getRevocableSubpacket() { - return revocable; - } - - @Override - public SignatureSubpackets addRevocationKey(@Nonnull PGPPublicKey revocationKey) { - return addRevocationKey(true, revocationKey); - } - - @Override - public SignatureSubpackets addRevocationKey(boolean isCritical, @Nonnull PGPPublicKey revocationKey) { - return addRevocationKey(isCritical, false, revocationKey); - } - - @Override - public SignatureSubpackets addRevocationKey(boolean isCritical, boolean isSensitive, @Nonnull PGPPublicKey revocationKey) { - byte clazz = (byte) 0x80; - clazz |= (isSensitive ? 0x40 : 0x00); - return addRevocationKey(new RevocationKey(isCritical, clazz, revocationKey.getAlgorithm(), revocationKey.getFingerprint())); - } - - @Override - public SignatureSubpackets addRevocationKey(@Nonnull RevocationKey revocationKey) { - this.revocationKeyList.add(revocationKey); - return this; - } - - @Override - public SignatureSubpackets clearRevocationKeys() { - revocationKeyList.clear(); - return this; - } - - public List getRevocationKeySubpackets() { - return new ArrayList<>(revocationKeyList); - } - - @Override - public SignatureSubpackets setRevocationReason(RevocationAttributes revocationAttributes) { - return setRevocationReason(false, revocationAttributes); - } - - @Override - public SignatureSubpackets setRevocationReason(boolean isCritical, RevocationAttributes revocationAttributes) { - return setRevocationReason(isCritical, revocationAttributes.getReason(), revocationAttributes.getDescription()); - } - - @Override - public SignatureSubpackets setRevocationReason(boolean isCritical, RevocationAttributes.Reason reason, @Nonnull String description) { - return setRevocationReason(new RevocationReason(isCritical, reason.code(), description)); - } - - @Override - public SignatureSubpackets setRevocationReason(@Nullable RevocationReason reason) { - this.revocationReason = reason; - return this; - } - - public RevocationReason getRevocationReasonSubpacket() { - return revocationReason; - } - - @Override - public SignatureSubpackets setSignatureTarget(@Nonnull PublicKeyAlgorithm keyAlgorithm, @Nonnull HashAlgorithm hashAlgorithm, @Nonnull byte[] hashData) { - return setSignatureTarget(true, keyAlgorithm, hashAlgorithm, hashData); - } - - @Override - public SignatureSubpackets setSignatureTarget(boolean isCritical, @Nonnull PublicKeyAlgorithm keyAlgorithm, @Nonnull HashAlgorithm hashAlgorithm, @Nonnull byte[] hashData) { - return setSignatureTarget(new SignatureTarget(isCritical, keyAlgorithm.getAlgorithmId(), hashAlgorithm.getAlgorithmId(), hashData)); - } - - @Override - public SignatureSubpackets setSignatureTarget(@Nullable SignatureTarget signatureTarget) { - this.signatureTarget = signatureTarget; - return this; - } - - public SignatureTarget getSignatureTargetSubpacket() { - return signatureTarget; - } - - @Override - public SignatureSubpackets setFeatures(Feature... features) { - return setFeatures(true, features); - } - - @Override - public SignatureSubpackets setFeatures(boolean isCritical, Feature... features) { - byte bitmask = Feature.toBitmask(features); - return setFeatures(new Features(isCritical, bitmask)); - } - - @Override - public SignatureSubpackets setFeatures(@Nullable Features features) { - this.features = features; - return this; - } - - public Features getFeaturesSubpacket() { - return features; - } - - @Override - public SignatureSubpackets setTrust(int depth, int amount) { - return setTrust(true, depth, amount); - } - - @Override - public SignatureSubpackets setTrust(boolean isCritical, int depth, int amount) { - return setTrust(new TrustSignature(isCritical, depth, amount)); - } - - @Override - public SignatureSubpackets setTrust(@Nullable TrustSignature trust) { - this.trust = trust; - return this; - } - - public TrustSignature getTrustSubpacket() { - return trust; - } - - @Override - public SignatureSubpackets addEmbeddedSignature(@Nonnull PGPSignature signature) throws IOException { - return addEmbeddedSignature(true, signature); - } - - @Override - public SignatureSubpackets addEmbeddedSignature(boolean isCritical, @Nonnull PGPSignature signature) throws IOException { - byte[] sig = signature.getEncoded(); - byte[] data; - - if (sig.length - 1 > 256) { - data = new byte[sig.length - 3]; - } - else { - data = new byte[sig.length - 2]; - } - - System.arraycopy(sig, sig.length - data.length, data, 0, data.length); - - return addEmbeddedSignature(new EmbeddedSignature(isCritical, false, data)); - } - - @Override - public SignatureSubpackets addEmbeddedSignature(@Nonnull EmbeddedSignature embeddedSignature) { - this.embeddedSignatureList.add(embeddedSignature); - return this; - } - - @Override - public SignatureSubpackets clearEmbeddedSignatures() { - this.embeddedSignatureList.clear(); - return this; - } - - public List getEmbeddedSignatureSubpackets() { - return new ArrayList<>(embeddedSignatureList); - } - - public SignatureSubpackets addResidualSubpacket(SignatureSubpacket subpacket) { - this.residualSubpackets.add(subpacket); - return this; - } - - public List getResidualSubpackets() { - return new ArrayList<>(residualSubpackets); - } - -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsHelper.java b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsHelper.java deleted file mode 100644 index 8af60a03..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsHelper.java +++ /dev/null @@ -1,196 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature.subpackets; - -import org.bouncycastle.bcpg.SignatureSubpacket; -import org.bouncycastle.bcpg.sig.EmbeddedSignature; -import org.bouncycastle.bcpg.sig.Exportable; -import org.bouncycastle.bcpg.sig.Features; -import org.bouncycastle.bcpg.sig.IntendedRecipientFingerprint; -import org.bouncycastle.bcpg.sig.KeyExpirationTime; -import org.bouncycastle.bcpg.sig.KeyFlags; -import org.bouncycastle.bcpg.sig.NotationData; -import org.bouncycastle.bcpg.sig.PolicyURI; -import org.bouncycastle.bcpg.sig.PreferredAlgorithms; -import org.bouncycastle.bcpg.sig.PrimaryUserID; -import org.bouncycastle.bcpg.sig.RegularExpression; -import org.bouncycastle.bcpg.sig.Revocable; -import org.bouncycastle.bcpg.sig.RevocationKey; -import org.bouncycastle.bcpg.sig.RevocationReason; -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.PGPSignatureSubpacketGenerator; -import org.bouncycastle.openpgp.PGPSignatureSubpacketVector; -import org.pgpainless.algorithm.Feature; -import org.pgpainless.algorithm.HashAlgorithm; -import org.pgpainless.algorithm.KeyFlag; -import org.pgpainless.algorithm.PublicKeyAlgorithm; -import org.pgpainless.key.util.RevocationAttributes; - -public class SignatureSubpacketsHelper { - - public static SignatureSubpackets applyFrom(PGPSignatureSubpacketVector vector, SignatureSubpackets subpackets) { - for (SignatureSubpacket subpacket : vector.toArray()) { - org.pgpainless.algorithm.SignatureSubpacket type = org.pgpainless.algorithm.SignatureSubpacket.requireFromCode(subpacket.getType()); - switch (type) { - case signatureCreationTime: - case issuerKeyId: - case issuerFingerprint: - // ignore, we override this anyways - break; - case signatureExpirationTime: - SignatureExpirationTime sigExpTime = (SignatureExpirationTime) subpacket; - subpackets.setSignatureExpirationTime(sigExpTime.isCritical(), sigExpTime.getTime()); - break; - case exportableCertification: - Exportable exp = (Exportable) subpacket; - subpackets.setExportable(exp.isCritical(), exp.isExportable()); - break; - case trustSignature: - TrustSignature trustSignature = (TrustSignature) subpacket; - subpackets.setTrust(trustSignature.isCritical(), trustSignature.getDepth(), trustSignature.getTrustAmount()); - break; - case revocable: - Revocable rev = (Revocable) subpacket; - subpackets.setRevocable(rev.isCritical(), rev.isRevocable()); - break; - case keyExpirationTime: - KeyExpirationTime keyExpTime = (KeyExpirationTime) subpacket; - subpackets.setKeyExpirationTime(keyExpTime.isCritical(), keyExpTime.getTime()); - break; - case preferredSymmetricAlgorithms: - subpackets.setPreferredSymmetricKeyAlgorithms((PreferredAlgorithms) subpacket); - break; - case revocationKey: - RevocationKey revocationKey = (RevocationKey) subpacket; - subpackets.addRevocationKey(revocationKey); - break; - case notationData: - NotationData notationData = (NotationData) subpacket; - subpackets.addNotationData(notationData.isCritical(), notationData.getNotationName(), notationData.getNotationValue()); - break; - case preferredHashAlgorithms: - subpackets.setPreferredHashAlgorithms((PreferredAlgorithms) subpacket); - break; - case preferredCompressionAlgorithms: - subpackets.setPreferredCompressionAlgorithms((PreferredAlgorithms) subpacket); - break; - case primaryUserId: - PrimaryUserID primaryUserID = (PrimaryUserID) subpacket; - subpackets.setPrimaryUserId(primaryUserID); - break; - case keyFlags: - KeyFlags flags = (KeyFlags) subpacket; - subpackets.setKeyFlags(flags.isCritical(), KeyFlag.fromBitmask(flags.getFlags()).toArray(new KeyFlag[0])); - break; - case signerUserId: - SignerUserID signerUserID = (SignerUserID) subpacket; - subpackets.setSignerUserId(signerUserID.isCritical(), signerUserID.getID()); - break; - case revocationReason: - RevocationReason reason = (RevocationReason) subpacket; - subpackets.setRevocationReason(reason.isCritical(), - RevocationAttributes.Reason.fromCode(reason.getRevocationReason()), - reason.getRevocationDescription()); - break; - case features: - Features f = (Features) subpacket; - subpackets.setFeatures(f.isCritical(), Feature.fromBitmask(f.getData()[0]).toArray(new Feature[0])); - break; - case signatureTarget: - SignatureTarget target = (SignatureTarget) subpacket; - subpackets.setSignatureTarget(target.isCritical(), - PublicKeyAlgorithm.requireFromId(target.getPublicKeyAlgorithm()), - HashAlgorithm.requireFromId(target.getHashAlgorithm()), - target.getHashData()); - break; - case embeddedSignature: - EmbeddedSignature embeddedSignature = (EmbeddedSignature) subpacket; - subpackets.addEmbeddedSignature(embeddedSignature); - break; - case intendedRecipientFingerprint: - IntendedRecipientFingerprint intendedRecipientFingerprint = (IntendedRecipientFingerprint) subpacket; - subpackets.addIntendedRecipientFingerprint(intendedRecipientFingerprint); - break; - case policyUrl: - PolicyURI policyURI = (PolicyURI) subpacket; - subpackets.setPolicyUrl(policyURI); - break; - case regularExpression: - RegularExpression regex = (RegularExpression) subpacket; - subpackets.setRegularExpression(regex); - break; - - case keyServerPreferences: - case preferredKeyServers: - case placeholder: - case preferredAEADAlgorithms: - case attestedCertification: - subpackets.addResidualSubpacket(subpacket); - break; - } - } - return subpackets; - } - - public static PGPSignatureSubpacketGenerator applyTo(SignatureSubpackets subpackets, PGPSignatureSubpacketGenerator generator) { - addSubpacket(generator, subpackets.getIssuerKeyIdSubpacket()); - addSubpacket(generator, subpackets.getIssuerFingerprintSubpacket()); - addSubpacket(generator, subpackets.getSignatureCreationTimeSubpacket()); - addSubpacket(generator, subpackets.getSignatureExpirationTimeSubpacket()); - addSubpacket(generator, subpackets.getExportableSubpacket()); - addSubpacket(generator, subpackets.getPolicyURI()); - addSubpacket(generator, subpackets.getRegularExpression()); - for (NotationData notationData : subpackets.getNotationDataSubpackets()) { - addSubpacket(generator, notationData); - } - for (IntendedRecipientFingerprint intendedRecipientFingerprint : subpackets.getIntendedRecipientFingerprintSubpackets()) { - addSubpacket(generator, intendedRecipientFingerprint); - } - for (RevocationKey revocationKey : subpackets.getRevocationKeySubpackets()) { - addSubpacket(generator, revocationKey); - } - addSubpacket(generator, subpackets.getSignatureTargetSubpacket()); - addSubpacket(generator, subpackets.getFeaturesSubpacket()); - addSubpacket(generator, subpackets.getKeyFlagsSubpacket()); - addSubpacket(generator, subpackets.getTrustSubpacket()); - addSubpacket(generator, subpackets.getPreferredCompressionAlgorithmsSubpacket()); - addSubpacket(generator, subpackets.getPreferredSymmetricKeyAlgorithmsSubpacket()); - addSubpacket(generator, subpackets.getPreferredHashAlgorithmsSubpacket()); - for (EmbeddedSignature embeddedSignature : subpackets.getEmbeddedSignatureSubpackets()) { - addSubpacket(generator, embeddedSignature); - } - addSubpacket(generator, subpackets.getSignerUserIdSubpacket()); - addSubpacket(generator, subpackets.getKeyExpirationTimeSubpacket()); - addSubpacket(generator, subpackets.getPrimaryUserIdSubpacket()); - addSubpacket(generator, subpackets.getRevocableSubpacket()); - addSubpacket(generator, subpackets.getRevocationReasonSubpacket()); - for (SignatureSubpacket subpacket : subpackets.getResidualSubpackets()) { - addSubpacket(generator, subpacket); - } - - return generator; - } - - private static void addSubpacket(PGPSignatureSubpacketGenerator generator, SignatureSubpacket subpacket) { - if (subpacket != null) { - generator.addCustomSubpacket(subpacket); - } - } - - public static PGPSignatureSubpacketVector toVector(SignatureSubpackets subpackets) { - PGPSignatureSubpacketGenerator generator = new PGPSignatureSubpacketGenerator(); - applyTo(subpackets, generator); - return generator.generate(); - } - - public static PGPSignatureSubpacketVector toVector(RevocationSignatureSubpackets subpackets) { - PGPSignatureSubpacketGenerator generator = new PGPSignatureSubpacketGenerator(); - applyTo((SignatureSubpackets) subpackets, generator); - return generator.generate(); - } -} 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 1105a813..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.java +++ /dev/null @@ -1,664 +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.Nonnull; -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.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.SignatureSubpacket; -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; -import org.pgpainless.key.OpenPgpFingerprint; -import org.pgpainless.key.OpenPgpV4Fingerprint; -import org.pgpainless.key.generation.type.KeyType; -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 @Nullable IssuerFingerprint getIssuerFingerprint(PGPSignature signature) { - return hashedOrUnhashed(signature, SignatureSubpacket.issuerFingerprint); - } - - /** - * Return the {@link IssuerFingerprint} subpacket of the signature into a {@link org.pgpainless.key.OpenPgpFingerprint}. - * 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 @Nullable OpenPgpFingerprint getIssuerFingerprintAsOpenPgpFingerprint(PGPSignature signature) { - IssuerFingerprint subpacket = getIssuerFingerprint(signature); - if (subpacket == null) { - return null; - } - - OpenPgpFingerprint fingerprint = null; - if (subpacket.getKeyVersion() == 4) { - fingerprint = new OpenPgpV4Fingerprint(subpacket.getFingerprint()); - } - - return fingerprint; - } - - /** - * 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 @Nullable 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 @Nullable 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 @Nullable 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 @Nullable SignatureCreationTime getSignatureCreationTime(PGPSignature signature) { - if (signature.getVersion() == 3) { - return new SignatureCreationTime(false, signature.getCreationTime()); - } - 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 @Nullable 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 @Nullable 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 @Nullable 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 @Nullable Date getKeyExpirationTimeAsDate(PGPSignature signature, PGPPublicKey signingKey) { - if (signature.getKeyID() != signingKey.getKeyID()) { - throw new IllegalArgumentException("Provided key (" + Long.toHexString(signingKey.getKeyID()) + ") did not create the signature (" + Long.toHexString(signature.getKeyID()) + ")"); - } - KeyExpirationTime subpacket = getKeyExpirationTime(signature); - if (subpacket == null) { - return null; - } - - return SignatureUtils.datePlusSeconds(signingKey.getCreationTime(), subpacket.getTime()); - } - - /** - * Calculate the duration in seconds until the key expires after creation. - * - * @param expirationDate new expiration date - * @param creationDate key creation time - * @return lifetime of the key in seconds - */ - public static long getKeyLifetimeInSeconds(@Nullable Date expirationDate, @Nonnull Date creationDate) { - long secondsToExpire = 0; // 0 means "no expiration" - if (expirationDate != null) { - if (creationDate.after(expirationDate)) { - throw new IllegalArgumentException("Key MUST NOT expire before being created. " + - "(creation: " + creationDate + ", expiration: " + expirationDate + ")"); - } - secondsToExpire = (expirationDate.getTime() - creationDate.getTime()) / 1000; - } - return secondsToExpire; - } - - /** - * 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 @Nullable 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 @Nullable 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 @Nonnull Set parsePreferredSymmetricKeyAlgorithms(PGPSignature signature) { - Set algorithms = new LinkedHashSet<>(); - PreferredAlgorithms preferences = getPreferredSymmetricAlgorithms(signature); - if (preferences != null) { - for (int code : preferences.getPreferences()) { - SymmetricKeyAlgorithm algorithm = SymmetricKeyAlgorithm.fromId(code); - if (algorithm != null) { - algorithms.add(algorithm); - } - } - } - return algorithms; - } - - /** - * Return the hash algorithm preferences from the signatures hashed area. - * - * @param signature signature - * @return hash algo prefs - */ - public static @Nullable 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 @Nonnull Set parsePreferredHashAlgorithms(PGPSignature signature) { - Set algorithms = new LinkedHashSet<>(); - PreferredAlgorithms preferences = getPreferredHashAlgorithms(signature); - if (preferences != null) { - for (int code : preferences.getPreferences()) { - HashAlgorithm algorithm = HashAlgorithm.fromId(code); - if (algorithm != null) { - algorithms.add(algorithm); - } - } - } - return algorithms; - } - - /** - * Return the compression algorithm preferences from the signatures hashed area. - * - * @param signature signature - * @return compression algo prefs - */ - public static @Nullable 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 @Nonnull Set parsePreferredCompressionAlgorithms(PGPSignature signature) { - Set algorithms = new LinkedHashSet<>(); - PreferredAlgorithms preferences = getPreferredCompressionAlgorithms(signature); - if (preferences != null) { - for (int code : preferences.getPreferences()) { - CompressionAlgorithm algorithm = CompressionAlgorithm.fromId(code); - if (algorithm != null) { - algorithms.add(algorithm); - } - } - } - return algorithms; - } - - /** - * Return the primary user-id subpacket from the signatures hashed area. - * - * @param signature signature - * @return primary user id - */ - public static @Nullable 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 @Nullable 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 @Nullable 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 @Nullable 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 @Nullable 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 @Nonnull 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 @Nonnull 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 signature - * @return unhashed notations - */ - public static @Nonnull 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 @Nonnull 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 @Nullable 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 @Nullable 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 @Nonnull 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 - * - * @throws PGPException in case the embedded signatures cannot be parsed - */ - public static @Nullable 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 @Nullable 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 @Nullable 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 @Nullable

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

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

P hashedOrUnhashed(PGPSignature signature, SignatureSubpacket type) { - P hashedSubpacket = hashed(signature, type); - return hashedSubpacket != null ? hashedSubpacket : unhashed(signature, 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 - */ - public static @Nullable

P getSignatureSubpacket(PGPSignatureSubpacketVector vector, SignatureSubpacket type) { - if (vector == null) { - // Almost never happens, but may be caused by broken signatures. - return null; - } - org.bouncycastle.bcpg.SignatureSubpacket[] allPackets = vector.getSubpackets(type.getCode()); - if (allPackets.length == 0) { - return null; - } - - org.bouncycastle.bcpg.SignatureSubpacket last = allPackets[allPackets.length - 1]; - return (P) last; - } - - /** - * Make sure that the given key type can carry the given key flags. - * - * @param type key type - * @param flags key flags - */ - public 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 AUTHENTICATION."); - } - } - - /** - * Make sure that a key of the given {@link PublicKeyAlgorithm} is able to carry the given key flags. - * - * @param algorithm key algorithm - * @param flags key flags - */ - public static void assureKeyCanCarryFlags(PublicKeyAlgorithm algorithm, KeyFlag... flags) { - final int mask = KeyFlag.toBitmask(flags); - - if (!algorithm.isSigningCapable() && KeyFlag.hasKeyFlag(mask, KeyFlag.CERTIFY_OTHER)) { - throw new IllegalArgumentException("Algorithm " + algorithm + " cannot be used with key flag CERTIFY_OTHER."); - } - - if (!algorithm.isSigningCapable() && KeyFlag.hasKeyFlag(mask, KeyFlag.SIGN_DATA)) { - throw new IllegalArgumentException("Algorithm " + algorithm + " cannot be used with key flag SIGN_DATA."); - } - - if (!algorithm.isEncryptionCapable() && KeyFlag.hasKeyFlag(mask, KeyFlag.ENCRYPT_COMMS)) { - throw new IllegalArgumentException("Algorithm " + algorithm + " cannot be used with key flag ENCRYPT_COMMS."); - } - - if (!algorithm.isEncryptionCapable() && KeyFlag.hasKeyFlag(mask, KeyFlag.ENCRYPT_STORAGE)) { - throw new IllegalArgumentException("Algorithm " + algorithm + " cannot be used with key flag ENCRYPT_STORAGE."); - } - - if (!algorithm.isSigningCapable() && KeyFlag.hasKeyFlag(mask, KeyFlag.AUTHENTICATION)) { - throw new IllegalArgumentException("Algorithm " + algorithm + " cannot be used with key flag AUTHENTICATION."); - } - } -} 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 87170638..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java +++ /dev/null @@ -1,587 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.util; - -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 javax.annotation.Nonnull; -import javax.annotation.Nullable; - -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.openpgp.operator.KeyFingerPrintCalculator; -import org.bouncycastle.util.io.Streams; -import org.pgpainless.algorithm.HashAlgorithm; -import org.pgpainless.decryption_verification.OpenPgpInputStream; -import org.pgpainless.key.OpenPgpFingerprint; - -/** - * Utility class for dealing with ASCII armored OpenPGP data. - */ -public final class ArmorUtils { - - // MessageIDs are 32 printable characters - private static final Pattern PATTERN_MESSAGE_ID = Pattern.compile("^\\S{32}$"); - - /** - * Constant armor key for comments. - */ - public static final String HEADER_COMMENT = "Comment"; - /** - * Constant armor key for program versions. - */ - public static final String HEADER_VERSION = "Version"; - /** - * Constant armor key for message IDs. Useful for split messages. - */ - public static final String HEADER_MESSAGEID = "MessageID"; - /** - * Constant armor key for used hash algorithms in clearsigned messages. - */ - public static final String HEADER_HASH = "Hash"; - /** - * Constant armor key for message character sets. - */ - public static final String HEADER_CHARSET = "Charset"; - - private ArmorUtils() { - - } - - /** - * Return the ASCII armored encoding of the given {@link PGPSecretKey}. - * - * @param secretKey secret key - * @return ASCII armored encoding - * - * @throws IOException in case of an io error - */ - @Nonnull - public static String toAsciiArmoredString(@Nonnull PGPSecretKey secretKey) - throws IOException { - MultiMap header = keyToHeader(secretKey.getPublicKey()); - return toAsciiArmoredString(secretKey.getEncoded(), header); - } - - /** - * Return the ASCII armored encoding of the given {@link PGPPublicKey}. - * - * @param publicKey public key - * @return ASCII armored encoding - * - * @throws IOException in case of an io error - */ - @Nonnull - public static String toAsciiArmoredString(@Nonnull PGPPublicKey publicKey) - throws IOException { - MultiMap header = keyToHeader(publicKey); - return toAsciiArmoredString(publicKey.getEncoded(), header); - } - - /** - * Return the ASCII armored encoding of the given {@link PGPSecretKeyRing}. - * - * @param secretKeys secret key ring - * @return ASCII armored encoding - * - * @throws IOException in case of an io error - */ - @Nonnull - public static String toAsciiArmoredString(@Nonnull PGPSecretKeyRing secretKeys) - throws IOException { - MultiMap header = keysToHeader(secretKeys); - return toAsciiArmoredString(secretKeys.getEncoded(), header); - } - - /** - * Return the ASCII armored encoding of the given {@link PGPPublicKeyRing}. - * - * @param publicKeys public key ring - * @return ASCII armored encoding - * - * @throws IOException in case of an io error - */ - @Nonnull - public static String toAsciiArmoredString(@Nonnull PGPPublicKeyRing publicKeys) - throws IOException { - MultiMap header = keysToHeader(publicKeys); - return toAsciiArmoredString(publicKeys.getEncoded(), header); - } - - /** - * Return the ASCII armored encoding of the given {@link PGPSecretKeyRingCollection}. - * The encoding will use per-key ASCII armors protecting each {@link PGPSecretKeyRing} individually. - * Those armors are then concatenated with newlines in between. - * - * @param secretKeyRings secret key ring collection - * @return ASCII armored encoding - * - * @throws IOException in case of an io error - */ - @Nonnull - public static String toAsciiArmoredString(@Nonnull 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(); - } - - /** - * Return the ASCII armored encoding of the given {@link PGPPublicKeyRingCollection}. - * The encoding will use per-key ASCII armors protecting each {@link PGPPublicKeyRing} individually. - * Those armors are then concatenated with newlines in between. - * - * @param publicKeyRings public key ring collection - * @return ascii armored encoding - * - * @throws IOException in case of an io error - */ - @Nonnull - public static String toAsciiArmoredString(@Nonnull 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(); - } - - /** - * Return the ASCII armored representation of the given detached signature. - * The signature will not be stripped of non-exportable subpackets or trust-packets. - * If you need to strip those (e.g. because the signature is intended to be sent to a third party), use - * {@link #toAsciiArmoredString(PGPSignature, boolean)} and provide

true
as boolean value. - * - * @param signature signature - * @return ascii armored string - * - * @throws IOException in case of an error in the {@link ArmoredOutputStream} - */ - @Nonnull - public static String toAsciiArmoredString(@Nonnull PGPSignature signature) throws IOException { - return toAsciiArmoredString(signature, false); - } - - /** - * 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 {@link ArmoredOutputStream} - */ - @Nonnull - public static String toAsciiArmoredString(@Nonnull PGPSignature signature, boolean export) - throws IOException { - return toAsciiArmoredString(signature.getEncoded(export)); - } - - /** - * Return the ASCII armored encoding of the given OpenPGP data bytes. - * - * @param bytes openpgp data - * @return ASCII armored encoding - * - * @throws IOException in case of an io error - */ - @Nonnull - public static String toAsciiArmoredString(@Nonnull byte[] bytes) - throws IOException { - return toAsciiArmoredString(bytes, null); - } - - /** - * 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 additionalHeaderValues header map - * @return ASCII armored encoding - * - * @throws IOException in case of an io error - */ - @Nonnull - public static String toAsciiArmoredString(@Nonnull byte[] bytes, - @Nullable MultiMap additionalHeaderValues) - throws IOException { - return toAsciiArmoredString(new ByteArrayInputStream(bytes), additionalHeaderValues); - } - - /** - * Return the ASCII armored encoding of the {@link InputStream} containing OpenPGP data. - * - * @param inputStream input stream of OpenPGP data - * @return ASCII armored encoding - * - * @throws IOException in case of an io error - */ - @Nonnull - public static String toAsciiArmoredString(@Nonnull InputStream inputStream) - throws IOException { - return toAsciiArmoredString(inputStream, null); - } - - /** - * Return the ASCII armored encoding of the OpenPGP data from the given {@link InputStream}. - * The ASCII armor will include armor headers from the given header map. - * - * @param inputStream input stream of OpenPGP data - * @param additionalHeaderValues ASCII armor header map - * @return ASCII armored encoding - * - * @throws IOException in case of an io error - */ - @Nonnull - public static String toAsciiArmoredString(@Nonnull InputStream inputStream, - @Nullable MultiMap additionalHeaderValues) - throws IOException { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - ArmoredOutputStream armor = toAsciiArmoredStream(out, additionalHeaderValues); - Streams.pipeAll(inputStream, armor); - armor.close(); - - return out.toString(); - } - - /** - * Return an {@link ArmoredOutputStream} prepared with headers for the given key ring, which wraps the given - * {@link OutputStream}. - * - * The armored output stream can be used to encode the key ring by calling {@link PGPKeyRing#encode(OutputStream)} - * with the armored output stream as an argument. - * - * @param keyRing key ring - * @param outputStream wrapped output stream - * @return armored output stream - */ - @Nonnull - public static ArmoredOutputStream toAsciiArmoredStream(@Nonnull PGPKeyRing keyRing, - @Nonnull OutputStream outputStream) { - MultiMap header = keysToHeader(keyRing); - return toAsciiArmoredStream(outputStream, header); - } - - /** - * Create an {@link ArmoredOutputStream} wrapping the given {@link OutputStream}. - * The armored output stream will be prepared with armor headers given by header. - * - * Note: Since the armored output stream is retrieved from {@link ArmoredOutputStreamFactory#get(OutputStream)}, - * 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 - */ - @Nonnull - public static ArmoredOutputStream toAsciiArmoredStream(@Nonnull OutputStream outputStream, - @Nullable 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; - } - - /** - * Generate a header map for ASCII armor from the given {@link PGPKeyRing}. - * - * @param keyRing key ring - * @return header map - */ - @Nonnull - private static MultiMap keysToHeader(@Nonnull PGPKeyRing keyRing) { - PGPPublicKey publicKey = keyRing.getPublicKey(); - return keyToHeader(publicKey); - } - - /** - * Generate a header map for ASCII armor from the given {@link PGPPublicKey}. - * The header map consists of a comment field of the keys pretty-printed fingerprint, - * as well as some optional user-id information (see {@link #setUserIdInfoOnHeader(MultiMap, PGPPublicKey)}. - * - * @param publicKey public key - * @return header map - */ - @Nonnull - private static MultiMap keyToHeader(@Nonnull PGPPublicKey publicKey) { - MultiMap header = new MultiMap<>(); - OpenPgpFingerprint fingerprint = OpenPgpFingerprint.of(publicKey); - - header.put(HEADER_COMMENT, fingerprint.prettyPrint()); - setUserIdInfoOnHeader(header, publicKey); - return header; - } - - /** - * Add user-id information to the header map. - * If the key is carrying at least one user-id, we add a comment for the probable primary user-id. - * If the key carries more than one user-id, we further add a comment stating how many further identities - * the key has. - * - * @param header header map - * @param publicKey public key - */ - private static void setUserIdInfoOnHeader(@Nonnull MultiMap header, - @Nonnull PGPPublicKey publicKey) { - Tuple idCount = getPrimaryUserIdAndUserIdCount(publicKey); - String primary = idCount.getA(); - int totalCount = idCount.getB(); - if (primary != null) { - header.put(HEADER_COMMENT, primary); - } - if (totalCount == 2) { - header.put(HEADER_COMMENT, "1 further identity"); - } else if (totalCount > 2) { - header.put(HEADER_COMMENT, String.format("%d further identities", totalCount - 1)); - } - } - - /** - * Determine a probable primary user-id, as well as the total number of user-ids on the given {@link PGPPublicKey}. - * This method is trimmed for efficiency and does not do any cryptographic validation of signatures. - * - * The key might not have any user-id at all, in which case {@link Tuple#getA()} will return null. - * The key might have some user-ids, but none of it marked as primary, in which case {@link Tuple#getA()} - * will return the first user-id of the key. - * - * @param publicKey public key - * @return tuple consisting of a primary user-id candidate, and the total number of user-ids on the key. - */ - @Nonnull - private static Tuple getPrimaryUserIdAndUserIdCount(@Nonnull PGPPublicKey publicKey) { - // Quickly determine the primary user-id + number of total user-ids - // NOTE: THIS METHOD DOES NOT CRYPTOGRAPHICALLY VERIFY THE SIGNATURES - // DO NOT RELY ON IT! - Iterator userIds = publicKey.getUserIDs(); - int countIdentities = 0; - String first = null; - String primary = null; - while (userIds.hasNext()) { - countIdentities++; - String userId = userIds.next(); - // remember the first user-id - if (first == null) { - first = userId; - } - - if (primary == null) { - Iterator signatures = publicKey.getSignaturesForID(userId); - while (signatures.hasNext()) { - PGPSignature signature = signatures.next(); - if (signature.getHashedSubPackets().isPrimaryUserID()) { - primary = userId; - break; - } - } - } - } - // It may happen that no user-id is marked as primary - // in that case print the first one - String printed = primary != null ? primary : first; - return new Tuple<>(printed, countIdentities); - } - - /** - * Add an ASCII armor header entry about the used hash algorithm into the {@link ArmoredOutputStream}. - * - * @param armor armored output stream - * @param hashAlgorithm hash algorithm - * - * @see - * RFC 4880 - OpenPGP Message Format §6.2. Forming ASCII Armor - */ - public static void addHashAlgorithmHeader(@Nonnull ArmoredOutputStream armor, - @Nonnull HashAlgorithm hashAlgorithm) { - armor.addHeader(HEADER_HASH, hashAlgorithm.getAlgorithmName()); - } - - /** - * Add an ASCII armor comment header entry into the {@link ArmoredOutputStream}. - * - * @param armor armored output stream - * @param comment free-text comment - * - * @see - * RFC 4880 - OpenPGP Message Format §6.2. Forming ASCII Armor - */ - public static void addCommentHeader(@Nonnull ArmoredOutputStream armor, - @Nonnull String comment) { - armor.addHeader(HEADER_COMMENT, comment); - } - - /** - * Add an ASCII armor message-id header entry into the {@link ArmoredOutputStream}. - * - * @param armor armored output stream - * @param messageId message id - * - * @see - * RFC 4880 - OpenPGP Message Format §6.2. Forming ASCII Armor - */ - public static void addMessageIdHeader(@Nonnull ArmoredOutputStream armor, - @Nonnull String messageId) { - if (!PATTERN_MESSAGE_ID.matcher(messageId).matches()) { - throw new IllegalArgumentException("MessageIDs MUST consist of 32 printable characters."); - } - armor.addHeader(HEADER_MESSAGEID, messageId); - } - - /** - * Extract all ASCII armor header values of type comment from the given {@link ArmoredInputStream}. - * - * @param armor armored input stream - * @return list of comment headers - */ - @Nonnull - public static List getCommentHeaderValues(@Nonnull ArmoredInputStream armor) { - return getArmorHeaderValues(armor, HEADER_COMMENT); - } - - /** - * Extract all ASCII armor header values of type message id from the given {@link ArmoredInputStream}. - * - * @param armor armored input stream - * @return list of message-id headers - */ - @Nonnull - public static List getMessageIdHeaderValues(@Nonnull ArmoredInputStream armor) { - return getArmorHeaderValues(armor, HEADER_MESSAGEID); - } - - /** - * Return all ASCII armor header values of type hash-algorithm from the given {@link ArmoredInputStream}. - * - * @param armor armored input stream - * @return list of hash headers - */ - @Nonnull - public static List getHashHeaderValues(@Nonnull ArmoredInputStream armor) { - return getArmorHeaderValues(armor, HEADER_HASH); - } - - /** - * Return a list of {@link HashAlgorithm} enums extracted from the hash header entries of the given - * {@link ArmoredInputStream}. - * - * @param armor armored input stream - * @return list of hash algorithms from the ASCII header - */ - @Nonnull - public static List getHashAlgorithms(@Nonnull 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; - } - - /** - * Return all ASCII armor header values of type version from the given {@link ArmoredInputStream}. - * - * @param armor armored input stream - * @return list of version headers - */ - @Nonnull - public static List getVersionHeaderValues(@Nonnull ArmoredInputStream armor) { - return getArmorHeaderValues(armor, HEADER_VERSION); - } - - /** - * Return all ASCII armor header values of type charset from the given {@link ArmoredInputStream}. - * - * @param armor armored input stream - * @return list of charset headers - */ - @Nonnull - public static List getCharsetHeaderValues(@Nonnull ArmoredInputStream armor) { - return getArmorHeaderValues(armor, HEADER_CHARSET); - } - - /** - * Return all ASCII armor header values of the given headerKey from the given {@link ArmoredInputStream}. - * - * @param armor armored input stream - * @param headerKey ASCII armor header key - * @return list of values for the header key - */ - @Nonnull - public static List getArmorHeaderValues(@Nonnull ArmoredInputStream armor, - @Nonnull 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 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 - */ - @Nonnull - public static InputStream getDecoderStream(@Nonnull InputStream inputStream) - throws IOException { - OpenPgpInputStream openPgpIn = new OpenPgpInputStream(inputStream); - if (openPgpIn.isAsciiArmored()) { - ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(openPgpIn); - return PGPUtil.getDecoderStream(armorIn); - } - - return openPgpIn; - } -} 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 e48fb4c0..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/util/ArmoredInputStreamFactory.java +++ /dev/null @@ -1,40 +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; - -/** - * Factory class for instantiating preconfigured {@link ArmoredInputStream ArmoredInputStreams}. - * {@link #get(InputStream)} will return an {@link ArmoredInputStream} that is set up to properly detect CRC errors. - */ -public final class ArmoredInputStreamFactory { - - private ArmoredInputStreamFactory() { - - } - - /** - * Return an instance of {@link ArmoredInputStream} which will detect CRC errors. - * - * @param inputStream input stream - * @return armored input stream - * @throws IOException in case of an IO error - */ - 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 f61bacb1..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/util/ArmoredOutputStreamFactory.java +++ /dev/null @@ -1,120 +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; -import org.pgpainless.encryption_signing.ProducerOptions; - -/** - * Factory to create configured {@link ArmoredOutputStream ArmoredOutputStreams}. - * The configuration entails setting custom version and comment headers. - */ -public final class ArmoredOutputStreamFactory { - - /** - * Name of the program. - */ - 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.clearHeaders(); - if (version != null && !version.isEmpty()) { - armoredOutputStream.setHeader(ArmorUtils.HEADER_VERSION, version); - } - - for (String comment : comment) { - ArmorUtils.addCommentHeader(armoredOutputStream, comment); - } - return armoredOutputStream; - } - - /** - * Return an instance of the {@link ArmoredOutputStream} which might have pre-populated armor headers. - * - * @param outputStream output stream - * @param options options - * @return armored output stream - */ - public static ArmoredOutputStream get(OutputStream outputStream, ProducerOptions options) { - if (options.isHideArmorHeaders()) { - ArmoredOutputStream armorOut = new ArmoredOutputStream(outputStream); - armorOut.clearHeaders(); - return armorOut; - } else { - return get(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 - */ - public static void setVersionInfo(String versionString) { - if (versionString == null) { - version = null; - return; - } - String trimmed = versionString.trim(); - if (trimmed.isEmpty()) { - version = null; - } else { - version = trimmed; - } - } - - /** - * 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. - * - * @see org.pgpainless.encryption_signing.ProducerOptions#setComment(String) for how to set comments for - * individual messages. - * - * @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/CRCingArmoredInputStreamWrapper.java b/pgpainless-core/src/main/java/org/pgpainless/util/CRCingArmoredInputStreamWrapper.java deleted file mode 100644 index 2c43339b..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/util/CRCingArmoredInputStreamWrapper.java +++ /dev/null @@ -1,153 +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; - -import javax.annotation.Nonnull; - -/** - * 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; - } - - @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(@Nonnull 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/DateUtil.java b/pgpainless-core/src/main/java/org/pgpainless/util/DateUtil.java deleted file mode 100644 index ad1fce09..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/util/DateUtil.java +++ /dev/null @@ -1,69 +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() { - - } - - // Java's SimpleDateFormat is not thread-safe, therefore we return a new instance on every invocation. - 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); - } - - /** - * Floor a date down to seconds precision. - * @param date date - * @return floored date - */ - public static Date toSecondsPrecision(Date date) { - long millis = date.getTime(); - long seconds = millis / 1000; - long floored = seconds * 1000; - return new Date(floored); - } - - /** - * Return the current date "floored" to UTC precision. - * - * @return now - */ - public static Date now() { - return toSecondsPrecision(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/Passphrase.java b/pgpainless-core/src/main/java/org/pgpainless/util/Passphrase.java deleted file mode 100644 index 9576fb3e..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/util/Passphrase.java +++ /dev/null @@ -1,168 +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 { - - public 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 (getChars() == null && other.getChars() == null) || - org.bouncycastle.util.Arrays.constantTimeAreEqual(getChars(), other.getChars()); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/SessionKey.java b/pgpainless-core/src/main/java/org/pgpainless/util/SessionKey.java deleted file mode 100644 index 1e71bb03..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/util/SessionKey.java +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.util; - -import javax.annotation.Nonnull; - -import org.bouncycastle.openpgp.PGPSessionKey; -import org.bouncycastle.util.encoders.Hex; -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; - -/** - * A {@link SessionKey} is the symmetric key that is used to encrypt/decrypt an OpenPGP message. - * The OpenPGP message header contains a copy of the session key, encrypted for the public key of each recipient. - */ -public class SessionKey { - - private final SymmetricKeyAlgorithm algorithm; - private final byte[] key; - - /** - * Constructor to create a session key from a BC {@link PGPSessionKey} object. - * - * @param sessionKey BC session key - */ - public SessionKey(@Nonnull PGPSessionKey sessionKey) { - this(SymmetricKeyAlgorithm.requireFromId(sessionKey.getAlgorithm()), sessionKey.getKey()); - } - - /** - * Create a session key object from an algorithm and a key. - * - * @param algorithm algorithm - * @param key key - */ - public SessionKey(@Nonnull SymmetricKeyAlgorithm algorithm, @Nonnull byte[] key) { - this.algorithm = algorithm; - this.key = key; - } - - /** - * Return the symmetric key algorithm. - * - * @return algorithm - */ - public SymmetricKeyAlgorithm getAlgorithm() { - return algorithm; - } - - /** - * Return the bytes of the key. - * - * @return key - */ - public byte[] getKey() { - byte[] copy = new byte[key.length]; - System.arraycopy(key, 0, copy, 0, copy.length); - return copy; - } - - @Override - public String toString() { - return "" + getAlgorithm().getAlgorithmId() + ":" + Hex.toHexString(getKey()); - } -} 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 68c9d946..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,6 +7,7 @@ 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; @@ -33,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 ac5e8065..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 @@ -6,6 +6,7 @@ package org.pgpainless.util.selection.keyring; import java.util.HashSet; import java.util.Iterator; +import java.util.Map; import java.util.Set; import javax.annotation.Nonnull; @@ -32,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 29c43c41..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,10 +4,11 @@ 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; @@ -29,9 +30,9 @@ public final class ExactUserId { @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; } @@ -45,9 +46,9 @@ public final class ExactUserId { @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/userid/SelectUserId.java b/pgpainless-core/src/main/java/org/pgpainless/util/selection/userid/SelectUserId.java deleted file mode 100644 index 5c1611af..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/util/selection/userid/SelectUserId.java +++ /dev/null @@ -1,231 +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; -import org.pgpainless.key.info.KeyRingInfo; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * Filter for selecting user-ids from keys and from lists. - */ -public abstract class SelectUserId { - - /** - * Return true, if the given user-id is accepted by this particular filter, false otherwise. - * - * @param userId user-id - * @return acceptance of the filter - */ - protected abstract boolean accept(String userId); - - /** - * Select all currently valid user-ids of the given key ring. - * - * @param keyRing public or secret key ring - * @return valid user-ids - */ - @Nonnull - public List selectUserIds(@Nonnull PGPKeyRing keyRing) { - List userIds = PGPainless.inspectKeyRing(keyRing).getValidUserIds(); - return selectUserIds(userIds); - } - - /** - * Select all acceptable (see {@link #accept(String)}) from the given list of user-ids. - * - * @param userIds list of user-ids - * @return sub-list of acceptable user-ids - */ - @Nonnull - public List selectUserIds(@Nonnull List userIds) { - List selected = new ArrayList<>(); - for (String userId : userIds) { - if (accept(userId)) { - selected.add(userId); - } - } - return selected; - } - - /** - * Return the first valid, acceptable user-id from the given public or secret key ring. - * - * @param keyRing public or secret key ring - * @return first matching valid user-id or null - */ - @Nullable - public String firstMatch(PGPKeyRing keyRing) { - return firstMatch(selectUserIds(keyRing)); - } - - /** - * Return the first valid, acceptable user-id from the list of user-ids. - * - * @param userIds list of user-ids - * @return first matching valid user-id or null - */ - @Nullable - public String firstMatch(@Nonnull List userIds) { - for (String userId : userIds) { - if (accept(userId)) { - return userId; - } - } - return null; - } - - /** - * Filter that filters for user-ids which contain the given
query
as a substring. - * - * @param query query - * @return filter - */ - public static SelectUserId containsSubstring(@Nonnull CharSequence query) { - return new SelectUserId() { - @Override - protected boolean accept(String userId) { - return userId.contains(query.toString()); - } - }; - } - - /** - * Filter that filters for user-ids which match the given
query
exactly. - * - * @param query query - * @return filter - */ - public static SelectUserId exactMatch(@Nonnull CharSequence query) { - return new SelectUserId() { - @Override - protected boolean accept(String userId) { - return userId.equals(query.toString()); - } - }; - } - - /** - * Filter that filters for user-ids which start with the given
substring
. - * - * @param substring substring - * @return filter - */ - public static SelectUserId startsWith(@Nonnull CharSequence substring) { - String string = substring.toString(); - return new SelectUserId() { - @Override - protected boolean accept(String userId) { - return userId.startsWith(string); - } - }; - } - - /** - * Filter that filters 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 ({@code
""
}), the result of the filter will be the same. - * - * @param email email address - * @return filter - */ - public static SelectUserId containsEmailAddress(@Nonnull CharSequence email) { - String string = email.toString(); - return containsSubstring(string.matches("^<.+>$") ? string : '<' + string + '>'); - } - - /** - * Filter that filters for valid user-ids on the given
keyRing
only. - * - * @param keyRing public / secret keys - * @return filter - */ - public static SelectUserId validUserId(PGPKeyRing keyRing) { - final KeyRingInfo info = PGPainless.inspectKeyRing(keyRing); - - return new SelectUserId() { - @Override - protected boolean accept(String userId) { - return info.isUserIdValid(userId); - } - }; - } - - /** - * Filter that filters for user-ids which pass all the given
filters
. - * - * @param filters filters - * @return filter - */ - public static SelectUserId and(SelectUserId... filters) { - return new SelectUserId() { - @Override - protected boolean accept(String userId) { - boolean accept = true; - for (SelectUserId filter : filters) { - accept &= filter.accept(userId); - } - return accept; - } - }; - } - - /** - * Filter that filters for user-ids which pass at least one of the given
filters
. - * - * @param filters filters - * @return filter - */ - public static SelectUserId or(SelectUserId... filters) { - return new SelectUserId() { - @Override - protected boolean accept(String userId) { - boolean accept = false; - for (SelectUserId filter : filters) { - accept |= filter.accept(userId); - } - return accept; - } - }; - } - - /** - * Filter that inverts the result of the given
filter
. - * - * @param filter filter - * @return inverting filter - */ - public static SelectUserId not(SelectUserId filter) { - return new SelectUserId() { - @Override - protected boolean accept(String userId) { - return !filter.accept(userId); - } - }; - } - - /** - * Filter that selects user-ids by the given
email
address. - * It returns user-ids which either contain the given
email
address as angle-bracketed string, - * or which equal the given
email
string exactly. - * - * @param email email - * @return filter - */ - public static SelectUserId byEmail(CharSequence email) { - return SelectUserId.or( - SelectUserId.exactMatch(email), - SelectUserId.containsEmailAddress(email) - ); - } -} 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/consumer/README.md b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/README.md similarity index 100% rename from pgpainless-core/src/main/java/org/pgpainless/signature/consumer/README.md rename to pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/README.md 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/test/java/investigations/KleopatraCompatibilityTest.java b/pgpainless-core/src/test/java/investigations/KleopatraCompatibilityTest.java deleted file mode 100644 index cb6a26b9..00000000 --- a/pgpainless-core/src/test/java/investigations/KleopatraCompatibilityTest.java +++ /dev/null @@ -1,272 +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 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.SymmetricKeyAlgorithm; -import org.pgpainless.decryption_verification.ConsumerOptions; -import org.pgpainless.decryption_verification.DecryptionStream; -import org.pgpainless.decryption_verification.MessageInspector; -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.implementation.ImplementationFactory; -import org.pgpainless.implementation.JceImplementationFactory; -import org.pgpainless.key.protection.SecretKeyRingProtector; - -public class KleopatraCompatibilityTest { - - public static final String KLEOPATRA_PUBKEY = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + - "\n" + - "mQGNBGF4StQBDADgAGvvtzCrSa5I9/jIZq0SKxoz7Hz61YM2Hs/hPedXfQeW7lrf\n" + - "qutyXSIb8L964v9u2RGnzteaPwciGSyoMal5teAPOsv6cp7kIDksQH8iJm/9FhoJ\n" + - "hFl2Yx5BX6sBtoXwY63Kf9Vpx/Std9tN34HHI7zrbO70rv6ZcDPFHyWoVdoDZOX1\n" + - "DWbBnOP3SoaNaPnbwEBfEkPwyN/NsnxTfe+IsCYC2byC3NZwYA5FscWFioeJ/UpF\n" + - "HMgZ6utn9mfTexOYEE0mL1mhrc7PbRjDlNasW3GLrpeVN55anT0jvtNXulG4POzG\n" + - "fJ8g3qddcbTXYhQItjurBlkYLV1JOhdCN83IJRect4EIKBkLuEKO0/a7bE6HC7nr\n" + - "PLw9MWGgcnDe2cTc4a6nAGC/eMeCONQlyAvOIEIXibbz4OB0dTNA5YYTMBHVO7n0\n" + - "GbNg8eqw+N+IijboLtJly+LshP81IdQMHg0h6K3+bfYV0rwC/XmR387s+pVpAp5k\n" + - "Lrw8Rt+BsQSY2O8AEQEAAbQhS2xlb3BhdHJhIDxrbGVvcGF0cmFAdGVzdC5kb21h\n" + - "aW4+iQHUBBMBCgA+FiEEzYzHEulLyE5PkaUp6EVgKKoTP1kFAmF4StQCGwMFCQPB\n" + - "7cwFCwkIBwMFFQoJCAsFFgIDAQACHgECF4AACgkQ6EVgKKoTP1nClwv/exOrk3H0\n" + - "aKOqwB6gMtA9bOJgYG4Lm3v9EM39mGScTJgAaZMlJIVMZ7qBUCEbw9lOJMZLguVm\n" + - "VJN8sVYE6zNdPGxmQLLciXDheGIdmQDi/K1j2ujKWUVZEvasiu7pO2Gcl3Kjqaeu\n" + - "dpvKtEDPUHtkqzTQHgxpQpSky58wubLyoX/bNnCky3M/tu774kJ2HGHTy6S4c1KH\n" + - "f6k4X96vP1V7yoKp+dukYLXwtm73JAi7nX/wOmoQI4I60fs26ZFDpoEkAZVjZtj6\n" + - "qfT9unS+XZeklc0siaZ5wZvVuSGWcI4v4/rA/ZU9KjDriatEu0ZzE/Xll1MHQyh4\n" + - "B31zjwP8LmLSrNHMLmT+7nM+nCfCoo71uZGkuuR0sKa6bToBUOls1olRmKaZf9NS\n" + - "JjW0K0xL3TEzduM7h+oDNLf5bSSZFoDGwsHRW6E53l7ZDe7tOH+ZGSDuCbIVu4dQ\n" + - "6k0NVMFI+gxTwQU/4RS3heRvn739P7VRLyUl4gX0/q8EanHPQX9NXIuSuQGNBGF4\n" + - "StQBDADMeuyDHP4np/ZnfaHXKLnz6C+6lrF/B0LhGXDxvN+cCpFvybmqGZ74DOkK\n" + - "VXVlmXjvb22p6+oOD163/KOqfrjKT/oeVhMglMc2raNy5+XWHcjKBhprxbX9bIhr\n" + - "QEjmvP57pIfQ83s1dgQsWlxIwX1g86X04u6tnG+fwNdGrhZwcbaivJT5F82uKKIq\n" + - "gtDbqcUtqOQpg+zUO2rdbgjWw5LZPBiC/dHkWydGvzWrnAgDmVAsJita2F+Pxwmn\n" + - "i3p5qU2hBJmJuVo15w6elST1Svn3jim5gqbXXhh2BwDSDPEp0uRZlV6r9RMlH+js\n" + - "4IvKiveGzdXTzmbPl8U+4HHynPM1TWRxCaXNF4w4Blnlqzgg0jFXVzV0tXk1HJTc\n" + - "P4Lmmo0xpf5OEsbCZv61qDJO20QMHw9Y9qU/lcCsXvmtFfEDTZSfvIEAlpo7tvIn\n" + - "3H94EiVc5FNpRfWrngwPnwt3m3QkmG3lkd5WnxuyjH/LbKMtuBC/3QuKNrrySvXF\n" + - "L4SL51cAEQEAAYkBvAQYAQoAJhYhBM2MxxLpS8hOT5GlKehFYCiqEz9ZBQJheErU\n" + - "AhsMBQkDwe3MAAoJEOhFYCiqEz9ZkhsL/itexY5+qkWjjGd8cLAtrJTzhQRlk6s7\n" + - "t7eBFSuTywlKC1f1wVpu5djOHTPH8H0JWMAAxtHQluk3IcQruBMFoao3xma+2HW1\n" + - "x4C0AfrL4C00zxUUxqtmfZi81NU0izmFNABdcEHGbE8jN86wIaiAnS1em61F+vju\n" + - "MTMLJVq56SQJhWSymf4z4d8gVIy7WzeSuHnHcDbMcCfFzN1kn2T/k5gav4wEcz3n\n" + - "LizUYsT+rFKizgVzSDLlSFcJQPd+a8Kwbo/hnzDt9zgmVirzU0/2Sgd0d6Iatplk\n" + - "YPzWmjATe3htmKrGXD4R/rF7aEnPCkR8k8WMLPleuenCRGQi5KKzNuevY2U8A4Mi\n" + - "KNt5EM8WdqcXD3Pv7nsVi4dNc8IK1TZ4BfN3YBFQL+hO/Fk7apiqZDu3sNpG7JR0\n" + - "V37ltHAK0HFdznyP79oixknV6pfdAVbIyzQXk/FqnpvbjCY4v/DWLz6a4n8tYQPh\n" + - "g94JEXpwhb9guKuzYzP/QeBp4qFu5FO87w==\n" + - "=Jz7i\n" + - "-----END PGP PUBLIC KEY BLOCK-----\n"; - public static final String KLEOPATRA_SECKEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + - "\n" + - "lQVYBGF4StQBDADgAGvvtzCrSa5I9/jIZq0SKxoz7Hz61YM2Hs/hPedXfQeW7lrf\n" + - "qutyXSIb8L964v9u2RGnzteaPwciGSyoMal5teAPOsv6cp7kIDksQH8iJm/9FhoJ\n" + - "hFl2Yx5BX6sBtoXwY63Kf9Vpx/Std9tN34HHI7zrbO70rv6ZcDPFHyWoVdoDZOX1\n" + - "DWbBnOP3SoaNaPnbwEBfEkPwyN/NsnxTfe+IsCYC2byC3NZwYA5FscWFioeJ/UpF\n" + - "HMgZ6utn9mfTexOYEE0mL1mhrc7PbRjDlNasW3GLrpeVN55anT0jvtNXulG4POzG\n" + - "fJ8g3qddcbTXYhQItjurBlkYLV1JOhdCN83IJRect4EIKBkLuEKO0/a7bE6HC7nr\n" + - "PLw9MWGgcnDe2cTc4a6nAGC/eMeCONQlyAvOIEIXibbz4OB0dTNA5YYTMBHVO7n0\n" + - "GbNg8eqw+N+IijboLtJly+LshP81IdQMHg0h6K3+bfYV0rwC/XmR387s+pVpAp5k\n" + - "Lrw8Rt+BsQSY2O8AEQEAAQAL/jBENv3Iud52umyzrfI0mZ9cFUHR994uqp67RezR\n" + - "Y2tpH/0IMCGY2THj2oktt3y5s/OFJ3ZCrhdo9FcHGKXHSa7Vn0l40GIPV6htPxSH\n" + - "cz1/Dct5ezPIxmQpmGfavuTYGQVC3TxQjkJEWTcVp/YgLn0j+L2708N6f5a9ZBJa\n" + - "E0mx8g+gKqLCd/1JGp/6+YI39/q/cr9plqUoC31ts7dj3/zSg+ZCV4nVHwnI0Np4\n" + - "o0iSoID9yIaa3I0lHwNgR1/82UVEla94QGKSRQqjTrgsTLPFIACNtSI/5iaPdKZK\n" + - "a01oic1LKGEpuqpHAbnPnCAKrtWODk8B/3U4CABflXufI3GTYOZeaGZvd6I/lx/t\n" + - "HQcg5SKE8vNIB1YZ2+rSsznAFmexaLjPVG3XhGQdBVoV/mmlcI71TUEcL9kXYMh6\n" + - "JnwH5/F2kG9JAXC+0Y3R9Ji+wabVGMUHxugcXpQa0d/malCZaS/dviDUfZ1KbDjH\n" + - "Jlzew7cmfRtiw4tfczboekeSbQYA6bh6IFqQlcW7qj34TAg8h8t34Q2o2U0VMj96\n" + - "OiG8B/LARF89ue4CaQQMQt16BHeMhePBAhPCkkCEtmbXremHsrtn6C149cS9GAEt\n" + - "fSAHVGedVDHzke/K84uHzBbY0dS6O6u2ApvWOutgWpB5Le4A7WTslyAdLWRZ1l69\n" + - "H2706M9fgGClVsQByCNVksDEbOizIlAkFOq0b39u8dnb063A9ReAuk/argCA7JHU\n" + - "j3BFIF5crIn+YrWl6slFuoXGWTXlBgD1WsVhU4hXJ5g35TOso0rEND/wHrjW0W4F\n" + - "LViA5yAt9sVLNGgm9Ye3YSVIHId6HiJQZmsWJb81WD5dvBLl74icZmfSWtRTwvCZ\n" + - "0k3rYlu3Ex4bQUwoyhSlDoPJ9YMaumd1yaM3nMeyrlaHYIpV8NtqSuqJc7i2iNX1\n" + - "3s9AotipHYEUOlsp936bNEuh0m8xXEZ2C8qjpNenymg8XfNd/IH2M4Sjzz+pN5sS\n" + - "gQt+pQhYFnW0Gersb/X3OsAtLtRE5kMF/3v7GAz7usMcajqbh9qB+Ytp4n1u3aQC\n" + - "ck1exVOwdLDZgsHfojO1SEFd3IafO01xp+TmS8qIoZvKJegM+qq9px1PHSTRnb4D\n" + - "8tuBxtdoUE7n+g3Li74je7+DEdcq6g9ZjgyeosCHGItUwTcCMqnHa+ikjQjsnnzu\n" + - "eSwvVSfMJQYyZrZ5qYgQZKcovkFDvgXiC/jqfDd6GeAfbxzL2cyAYWvUdGln79O3\n" + - "Tc7ZWd0Xn6IaMPVPRBvH4RsaWqFdO0pIOuH7tCFLbGVvcGF0cmEgPGtsZW9wYXRy\n" + - "YUB0ZXN0LmRvbWFpbj6JAdQEEwEKAD4WIQTNjMcS6UvITk+RpSnoRWAoqhM/WQUC\n" + - "YXhK1AIbAwUJA8HtzAULCQgHAwUVCgkICwUWAgMBAAIeAQIXgAAKCRDoRWAoqhM/\n" + - "WcKXC/97E6uTcfRoo6rAHqAy0D1s4mBgbgube/0Qzf2YZJxMmABpkyUkhUxnuoFQ\n" + - "IRvD2U4kxkuC5WZUk3yxVgTrM108bGZAstyJcOF4Yh2ZAOL8rWPa6MpZRVkS9qyK\n" + - "7uk7YZyXcqOpp652m8q0QM9Qe2SrNNAeDGlClKTLnzC5svKhf9s2cKTLcz+27vvi\n" + - "QnYcYdPLpLhzUod/qThf3q8/VXvKgqn526RgtfC2bvckCLudf/A6ahAjgjrR+zbp\n" + - "kUOmgSQBlWNm2Pqp9P26dL5dl6SVzSyJpnnBm9W5IZZwji/j+sD9lT0qMOuJq0S7\n" + - "RnMT9eWXUwdDKHgHfXOPA/wuYtKs0cwuZP7ucz6cJ8KijvW5kaS65HSwprptOgFQ\n" + - "6WzWiVGYppl/01ImNbQrTEvdMTN24zuH6gM0t/ltJJkWgMbCwdFboTneXtkN7u04\n" + - "f5kZIO4JshW7h1DqTQ1UwUj6DFPBBT/hFLeF5G+fvf0/tVEvJSXiBfT+rwRqcc9B\n" + - "f01ci5KdBVgEYXhK1AEMAMx67IMc/ien9md9odcoufPoL7qWsX8HQuEZcPG835wK\n" + - "kW/JuaoZnvgM6QpVdWWZeO9vbanr6g4PXrf8o6p+uMpP+h5WEyCUxzato3Ln5dYd\n" + - "yMoGGmvFtf1siGtASOa8/nukh9DzezV2BCxaXEjBfWDzpfTi7q2cb5/A10auFnBx\n" + - "tqK8lPkXza4ooiqC0NupxS2o5CmD7NQ7at1uCNbDktk8GIL90eRbJ0a/NaucCAOZ\n" + - "UCwmK1rYX4/HCaeLenmpTaEEmYm5WjXnDp6VJPVK+feOKbmCptdeGHYHANIM8SnS\n" + - "5FmVXqv1EyUf6Ozgi8qK94bN1dPOZs+XxT7gcfKc8zVNZHEJpc0XjDgGWeWrOCDS\n" + - "MVdXNXS1eTUclNw/guaajTGl/k4SxsJm/rWoMk7bRAwfD1j2pT+VwKxe+a0V8QNN\n" + - "lJ+8gQCWmju28ifcf3gSJVzkU2lF9aueDA+fC3ebdCSYbeWR3lafG7KMf8tsoy24\n" + - "EL/dC4o2uvJK9cUvhIvnVwARAQABAAv9ExmcWWGY6p1e1StACyKrvqO+lEBFPidb\n" + - "Jj7udODT8PXFFgW9c60cU0aUHLn/fZ5d/zI6XSKYj02nkaoQo6QIoM/i/iMY0En1\n" + - "aHRvDb7+51w1iDa/uwy8biVNgi8pYBw2l9gLiQdlR94ej6y1GBAIJR6ShD26VmSE\n" + - "F2O3osuEybtleEKt660/MiMWMBWzaqwAq2jY6c5/4xHVw+87oMv4k0AbeLOQKojK\n" + - "h2o5mi5jSpVvOWCAsOYAhHlEEUQPDFQ1rbJ3P3XcRZE4EIxP2eKDyfyOXRTihLDl\n" + - "/9hqOf57wo0C43bnc1BkD6sk+ptKgUifpUHHejg/i7HINFivh7jCgCtoskf2P9BL\n" + - "WFuaPZVLQSVE5X2PsgeIYK9/eGeNxfXgtwRyUd8DtBge11tsMaENUTm39p36my2K\n" + - "jBgoEdBIQo1Mpi1EZba+L6pyw9bPFnj5H+opSe+X9/spkS9DyPOPGY7rCSTgv+7q\n" + - "Ph2WbtRRJslitLEjT9tNgwMRGWsgdbcpBgDgzujDUQb1coCdgw1gsQSTPir9hJxF\n" + - "Q+2DAbGpkqiYayHJqH7T9wGhiY8QoqLIejNawkrel4yTtYmM7pgtURWrkzz/jHAT\n" + - "3NNRTyvFqMmjwOIoV83tW8247uA8eofc981wEVayJ4y/KDcvU04FBrjCEoOUQMXw\n" + - "Ychr4cGiEckGBxAib6fVxjsU3PUIuUDpm9NC53Rc0GmwlduiZSJqRZQRgytLxWdM\n" + - "Va4c5oHdc0qpjCgk5qkW/09lI5kxTlMk3E0GAOjZ+HSQV8rI7qu7G0Qno9MP0Q49\n" + - "Qo5Hf4uV+I/6Iim/Eq5LWKmmairIob47jLtmhoIo7LArTm+9NsThFidc6wjRYgtT\n" + - "kGx4KUTEl8d0/mHV8GBzNNyRM7UOoLVjgf4tljFa8d2hQNMXZyBsIkLyoL6cL2sx\n" + - "aMZWl9jjh0bYE4TiTDIO1cfddxGjCPG9i12Z+yMl5p0g+r+IUAbuSh4+Yo7PUIKF\n" + - "8v+mqZRC9M9C/T/qOAB2gL2vDEZ4TdLAZfYUMwX9E/I1e0gHPlqXmQ/znTkjuCXd\n" + - "JopVXmvvku8SvVFb4pcW1k5Tk3iEj7nilQ64I5bONFUot+qKTtxAM2Fwxo0EjFZD\n" + - "TCP5RbY60iJcnhpk5mDGD41O1xe2HBkJw8dC5xUr1pPs+7Y8gMXN3qK4JcrLfLSO\n" + - "pOb623ir9jtJWLjv1wOvr7KsWZxg8XOQq8+AkEprUjb8v8WsJY5c7L8vSJ5OYlOP\n" + - "gv9Tj3MVmV1jGhH9pR+zGcclyathY3Ytloy1zZxR3WCJAbwEGAEKACYWIQTNjMcS\n" + - "6UvITk+RpSnoRWAoqhM/WQUCYXhK1AIbDAUJA8HtzAAKCRDoRWAoqhM/WZIbC/4r\n" + - "XsWOfqpFo4xnfHCwLayU84UEZZOrO7e3gRUrk8sJSgtX9cFabuXYzh0zx/B9CVjA\n" + - "AMbR0JbpNyHEK7gTBaGqN8Zmvth1tceAtAH6y+AtNM8VFMarZn2YvNTVNIs5hTQA\n" + - "XXBBxmxPIzfOsCGogJ0tXputRfr47jEzCyVauekkCYVkspn+M+HfIFSMu1s3krh5\n" + - "x3A2zHAnxczdZJ9k/5OYGr+MBHM95y4s1GLE/qxSos4Fc0gy5UhXCUD3fmvCsG6P\n" + - "4Z8w7fc4JlYq81NP9koHdHeiGraZZGD81powE3t4bZiqxlw+Ef6xe2hJzwpEfJPF\n" + - "jCz5XrnpwkRkIuSiszbnr2NlPAODIijbeRDPFnanFw9z7+57FYuHTXPCCtU2eAXz\n" + - "d2ARUC/oTvxZO2qYqmQ7t7DaRuyUdFd+5bRwCtBxXc58j+/aIsZJ1eqX3QFWyMs0\n" + - "F5Pxap6b24wmOL/w1i8+muJ/LWED4YPeCRF6cIW/YLirs2Mz/0HgaeKhbuRTvO8=\n" + - "=cgLL\n" + - "-----END PGP PRIVATE KEY BLOCK-----\n"; - // signed and encrypted - public static final String KLEOPATRA_MESSAGE = "-----BEGIN PGP MESSAGE-----\n" + - "\n" + - "hQGMA2gglaIyvWSdAQv+NjugJ3Sqk7F9PnVOphT8TNc1i1rHlU9bDDeyZ2Czl6KA\n" + - "YXwSP5KmwgTJH+vt9N5xrbKOGCuSCJNeb0wzH/YpQHLL5Hx5Pk0KtNH8BCevApkM\n" + - "Rcn4EKiXMmTFyib0fCPlqvEvqdD1ni1IliHNLxR/TYCSxbmu3TqPie70PiLsB32l\n" + - "6QKDi1U3HftsZOLLgIPbd1IqnSMeT3E15oD8LTQe3k/CV+huA54wrIeqDxfJpcAu\n" + - "rvb4rLVvGmaF67FXekMEDjD3cdk2m6WJ8c1myh3EUpDRlMPobhgeEV+h28heGuhu\n" + - "g2Id97DMfUhxypGbQ/rlwHE3UMvdW3YS0KRT7UfPee0F2m737b/aWO341LOzJz94\n" + - "xggPafIC6IseQQVZirocG1CLl0lauWZoXbfmzrXCT+YGNuaNjlE01BYPBjgEygle\n" + - "7Kur60YkB0H6fACskcudWDRFTsjEgIZa3riHou7XmvqupvJC+hyYdH3QqyFMvdix\n" + - "03/E9ePUs051Bvzn+a/dhQGMAwAAAAAAAAAAAQv/RtljqQ2BsB0KkdzZtnfY+CZZ\n" + - "PBYvloxplK+Bs8aoLVujyI7g6weOvFD49tSowsvJ//DleDpcKe4UZA/WRj5HlB1J\n" + - "5zLK5qlWb8El6QKlwEKB02zHDv244Bm9ZROnSK3CrEqRcfdBQIx4ThEOZlG0cE60\n" + - "iTbrda2SYUDpHh4Re/qhw/wvc0uUf+59u8WU5AIpgfLBU2fNEjOr6LMIsR3Edvf8\n" + - "zIFUrHlfvKQaAnZYU79dA0ZnTYgLiwMWB19nvhnSIdWAC3tJUsiuEIzEzA+vVbG/\n" + - "YrTOMR+vFm+dVOcVanzn0vnV0n8+np1kM1V2JgGRKV6XybS1oUbNPvpv79FBgfPi\n" + - "F3WghBHZf9lTaj4w7LtQSojvC0YxSoxfTif/MMxNZUoexQbk0jE97ibeFk6rqrBn\n" + - "46G2WbrrReDyOUekSkM5MQ/bZ1GJuFfC+kGyHETBejsfn0ZKa2RUla9k3vYFcDJC\n" + - "Et7Vwv81SF4yzvSwiV0rFx1RcyZaGlJumjCkqaHHhQIMAwAAAAAAAAAAAQ/+KTsY\n" + - "vnPMOmjmLqu7BQwx3jUaEmCXTurv5XHMbAEcq0UAwHJ/XAcJe7B1707Fu1sSJJjV\n" + - "3rJVHGUv7+APg5/vALx/FGlKk/12M8NhgOreLCLa/vQ9NmNrcfqGQdZtpk6OQxLv\n" + - "DcnCbUjyTO2IFRjmzEy8d8rne/FMC1MZD9hY1IboJWk5fN1NCsIIbPn3OSqVIaa1\n" + - "9fJj6D7SGacraBNJwl0x22ipV4yLtpo2DtnPJGK4xgTm0eW7eUK3nIfC/foHEgxG\n" + - "Bny1axnKqC9TFhAQ7Eo+Wh9eAiXtFBY7po7tfYmhb6mHBMAfYsVvCCyLNqUbXiV9\n" + - "kXWMBf0yxtNQlkx1jK+iqfGBm3EfHKncXGfl6zxwkh1FZXcY2EyCavkGND+3Gexg\n" + - "vbCUltulq1Fv1WjOGz9Icc5pK9AyjUuc/AQ4k7WhCVhCmbpsb/Cq6LsiqOC219dE\n" + - "r5TLGr+K1289PVOgbd06BL5NVP6qeO5fyWUA/Bs+exxqEDKce0f0ppKkcGNAv9p/\n" + - "Lg57FxT8aYVBgSoTv1DASquZANrO3kp7M3nC5lVzUldz8aS4YEirLLTF0MBnZEZ8\n" + - "MRcG8h8oSKozw+cuJXNF+bFiKM0wwRyw0AXGt69/lrPlWKMCfuK3n8vqxVPJ78JD\n" + - "ut8xHNWelqS2uO0qinvfbBcKzptYUm8ctNbHlSLS6QEnmjoiF/jobEDWsp6yBaym\n" + - "o7h9VQrmCKjKsoQzoF5KYHW87BLb2YRnx5WwTvN1BvZTNqNjkm9tuDTIwhTUx/L/\n" + - "B8l+KqpGcrmsldQ/pF/W3m2mFlsqpb02uWJSpXQ7NEavjvPThKPJHUnni4YtCg5b\n" + - "v8Zy/zvYgGj5y4DDjM84Xw/HcMdyHsWIcGosZ6W/jJhO7sECXqS6HoF5zFsIBPX9\n" + - "dEM4GS5TapLe0s7DyC0bK7VbPgLMBxPmbBSVp3O72qKpvgc6PAggTJHNhd6MLsJA\n" + - "JAiAOF/KNNZxSdMWIXqMyMviSPeU9+KclOG7iiR75Q5kIbpj9hWo5ullxr6XrHl2\n" + - "HFR+5jnmbSNwz/cf0vwkTnNG/Crofyy0kPTfGp5Ku4hp0wIhWXM9f8m7tuoxI3ep\n" + - "uNwB7FOs3xemsxAmoufyWcsyxnVf/3OJLWejIcIK1v3NmoiSxFQXl2cmiRVLTtAT\n" + - "oNjUT9QDQiyi8YR+CepV6RnBSmRomr7HfRAoACaCg6ToaXm0Dc8OQSge2X80ifdD\n" + - "NUcfhQAivaVAqhAogUIaPp9yqwTWaZ00N5cPH4HItPJtukb+Fsove2SoF+iPQre6\n" + - "hDjZCNyfUjT+wnca315nN+9D6Z1JgV5YEM23sFKp4M732Zdb5JlR0DXfDEuQH1NL\n" + - "hXOcpr9LpAvASH7weiVTEYxNz5KzFkUQA5YKLLeDwtcK\n" + - "=MgH4\n" + - "-----END PGP MESSAGE-----\n"; - public static final String PGPAINLESS_MESSAGE = "-----BEGIN PGP MESSAGE-----\n" + - "Version: PGPainless\n" + - "\n" + - "hQGMA2gglaIyvWSdAQv/Y9Wx763qAM95+teCUPPNRc5Iwqbc5uFjxfbcwsHIWdiZ\n" + - "n2wHNUmd2dUqYgpqOcBwZ/fUJuoHj/uXKZ1pbz2QSVYaL9MulKpgWiVAo0K2w0Oc\n" + - "97KfZ0d66tcZIhslVpZW06+lXuwMyjjjExe32fAkPFnYyTNORljyYlb/RDSkh7Ke\n" + - "Q+48fLR2kitV0WyRZ+d9cMfx2+D2gWYiaFGek9SrhI8L+nNd4UKvM4K4sSq4JHYf\n" + - "DCxGPWYOaTculuX8dfDh3ftHbrmL2Ca7Iv4NB0kSduG8Gin2OWyeSIPIwpF2ci9g\n" + - "cIBssAYhmS88FQit5pW9z2RZ/e9XmYIP++kz3/EdI6DqkiPUv1fiHTrJBC93LvVg\n" + - "pq75h9RNFuUlqR09SVuB/uZB6tYgv77vy5lPFo+wmLjM41aS4+qI1hBI3Ym4XTc1\n" + - "spPA0sEHtQTQ/xRNYqGpwunJniMF3ukWpOB6UNvQld+p2lj8czexhEAcne1cjey/\n" + - "f0/WUnluSt0HIg8Mnd7s0ukBhb4YxjvARjuqi6PikGz4JPshRwB8dPtS9FQiRxL7\n" + - "obaPHXlmLwohEtT3akzoIj/9C3Y7qnfreSllDgRDxRVFPXy5QnQqpsTy2JuJ4cvo\n" + - "p55RE2kyJ3vBZlB6T53pSgC00hQnNxoqgy7aejRItlec7zx5DnEg8t4rA7LYEGLT\n" + - "MBLWbTRc/njH6GTyc/3x7j9k8V83exqpF6fXrE3GP1C3fBxHY2S9/5BFAlzimplz\n" + - "Mow4S15D04EllRRk6f9HKY598xS4QlDEW/f3utwkQ8+/lNqesVuV8n76WDldMv2O\n" + - "5gTqAZ/pKhDKRLY6km4B2+2IAt2zg+V141wryHJgE/4VyUbu7zZxDIcDouuATQvt\n" + - "wNMnntqy3NTbM7DefSiYe9IUsTUz/g0VQJikoJx+rdX6YzQnRk/cmwvELnskQjSk\n" + - "aGd92A4ousaM299IOkbpLvFaJdrs7cLH0rEQTG5S3tRJSLEnjr94BUVtpIhQDo3i\n" + - "455UahKcCx/KhyIzo+8OdH0TYZf5ZFGLdTrqgi0ybAHcLrXkM+g2JOsst99CeRUq\n" + - "f/T4oFvuDSlLU56iWlLVE7gvDBibXfWIJ65YBHY4ueEzBC/3xOVj+dmTM2JfUSX7\n" + - "mqD25NaDCOuN4WhJmZHC1wyipj3KYT2bLg4gasHr/LvEI+Df/DREdXtrYAqPqZYU\n" + - "0QuubMF4n3hMqmu2wA==\n" + - "=fMRM\n" + - "-----END PGP MESSAGE-----"; - - @Test - public void testMessageDecryptionAndVerification() throws PGPException, IOException { - ImplementationFactory.setFactoryImplementation(new JceImplementationFactory()); - PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KLEOPATRA_SECKEY); - PGPPublicKeyRing publicKeys = PGPainless.readKeyRing().publicKeyRing(KLEOPATRA_PUBKEY); - - ConsumerOptions options = new ConsumerOptions() - .addDecryptionKey(secretKeys) - .addVerificationCert(publicKeys); - - ByteArrayOutputStream out = new ByteArrayOutputStream(); - DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() - .onInputStream(new ByteArrayInputStream(KLEOPATRA_MESSAGE.getBytes(StandardCharsets.UTF_8))) - .withOptions(options); - - Streams.pipeAll(decryptionStream, out); - decryptionStream.close(); - } - - @Test - public void testEncryptAndSignMessage() throws PGPException, IOException { - PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KLEOPATRA_SECKEY); - PGPPublicKeyRing publicKeys = PGPainless.readKeyRing().publicKeyRing(KLEOPATRA_PUBKEY); - - ProducerOptions options = ProducerOptions.signAndEncrypt( - EncryptionOptions.encryptCommunications() - .addRecipient(publicKeys) - .overrideEncryptionAlgorithm(SymmetricKeyAlgorithm.AES_128), - SigningOptions.get() - .addInlineSignature(SecretKeyRingProtector.unprotectedKeys(), secretKeys, DocumentSignatureType.BINARY_DOCUMENT) - ); - - ByteArrayOutputStream out = new ByteArrayOutputStream(); - EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() - .onOutputStream(out) - .withOptions(options); - - ByteArrayInputStream in = new ByteArrayInputStream("Hallo, Welt!\n\n".getBytes(StandardCharsets.UTF_8)); - Streams.pipeAll(in, encryptionStream); - encryptionStream.close(); - } - - @Test - public void testMessageInspection() throws PGPException, IOException { - MessageInspector.EncryptionInfo info = MessageInspector.determineEncryptionInfoForMessage( - new ByteArrayInputStream(KLEOPATRA_MESSAGE.getBytes(StandardCharsets.UTF_8))); - } -} diff --git a/pgpainless-core/src/test/java/investigations/OnePassSignatureVerificationWithPartialLengthLiteralDataRegressionTest.java b/pgpainless-core/src/test/java/investigations/OnePassSignatureVerificationWithPartialLengthLiteralDataRegressionTest.java index a9b20e9e..7ec53edb 100644 --- a/pgpainless-core/src/test/java/investigations/OnePassSignatureVerificationWithPartialLengthLiteralDataRegressionTest.java +++ b/pgpainless-core/src/test/java/investigations/OnePassSignatureVerificationWithPartialLengthLiteralDataRegressionTest.java @@ -130,6 +130,6 @@ public class OnePassSignatureVerificationWithPartialLengthLiteralDataRegressionT Streams.pipeAll(decryptionStream, out); decryptionStream.close(); - decryptionStream.getResult(); + decryptionStream.getMetadata(); } } 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/CachingBcPublicKeyDataDecryptorFactoryTest.java b/pgpainless-core/src/test/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java index e57df5d9..10cf4b1f 100644 --- a/pgpainless-core/src/test/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java +++ b/pgpainless-core/src/test/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java @@ -4,11 +4,11 @@ package org.bouncycastle; +import static org.junit.jupiter.api.Assertions.assertEquals; + import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPrivateKey; @@ -18,6 +18,7 @@ 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; @@ -25,8 +26,6 @@ import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnlockSecretKey; -import static org.junit.jupiter.api.Assertions.assertEquals; - public class CachingBcPublicKeyDataDecryptorFactoryTest { private static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + @@ -64,7 +63,7 @@ public class CachingBcPublicKeyDataDecryptorFactoryTest { "-----END PGP MESSAGE-----"; @Test - public void test() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + public void test() throws PGPException, IOException { PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); diff --git a/pgpainless-core/src/test/java/org/pgpainless/algorithm/RevocationStateTest.java b/pgpainless-core/src/test/java/org/pgpainless/algorithm/RevocationStateTest.java index e23d92d3..f5189eb9 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/algorithm/RevocationStateTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/algorithm/RevocationStateTest.java @@ -22,6 +22,13 @@ 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(); @@ -60,11 +67,6 @@ public class RevocationStateTest { assertEquals("softRevoked (2022-08-03 18:26:35 UTC)", state.toString()); } - @Test - public void testSoftRevokedNullDateThrows() { - assertThrows(NullPointerException.class, () -> RevocationState.softRevoked(null)); - } - @Test public void orderTest() { assertEquals(RevocationState.notRevoked(), RevocationState.notRevoked()); 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/decryption_verification/CanonicalizedDataEncryptionTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CanonicalizedDataEncryptionTest.java index c427af99..f9936db0 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CanonicalizedDataEncryptionTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CanonicalizedDataEncryptionTest.java @@ -13,6 +13,7 @@ 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; @@ -30,6 +31,9 @@ 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; @@ -122,9 +126,9 @@ public class CanonicalizedDataEncryptionTest { @Test public void noInputEncodingBinaryDataBinarySig() throws PGPException, IOException { String msg = encryptAndSign(message, DocumentSignatureType.BINARY_DOCUMENT, StreamEncoding.BINARY, false); - OpenPgpMetadata metadata = decryptAndVerify(msg); + MessageMetadata metadata = decryptAndVerify(msg); - if (!metadata.isVerified()) { + if (!metadata.isVerifiedSigned()) { // CHECKSTYLE:OFF System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); System.out.println(msg); @@ -136,9 +140,9 @@ public class CanonicalizedDataEncryptionTest { @Test public void noInputEncodingBinaryDataTextSig() throws PGPException, IOException { String msg = encryptAndSign(message, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT, StreamEncoding.BINARY, false); - OpenPgpMetadata metadata = decryptAndVerify(msg); + MessageMetadata metadata = decryptAndVerify(msg); - if (!metadata.isVerified()) { + if (!metadata.isVerifiedSigned()) { // CHECKSTYLE:OFF System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); System.out.println(msg); @@ -150,9 +154,9 @@ public class CanonicalizedDataEncryptionTest { @Test public void noInputEncodingTextDataBinarySig() throws PGPException, IOException { String msg = encryptAndSign(message, DocumentSignatureType.BINARY_DOCUMENT, StreamEncoding.TEXT, false); - OpenPgpMetadata metadata = decryptAndVerify(msg); + MessageMetadata metadata = decryptAndVerify(msg); - if (!metadata.isVerified()) { + if (!metadata.isVerifiedSigned()) { // CHECKSTYLE:OFF System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); System.out.println(msg); @@ -164,9 +168,9 @@ public class CanonicalizedDataEncryptionTest { @Test public void noInputEncodingTextDataTextSig() throws PGPException, IOException { String msg = encryptAndSign(message, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT, StreamEncoding.TEXT, false); - OpenPgpMetadata metadata = decryptAndVerify(msg); + MessageMetadata metadata = decryptAndVerify(msg); - if (!metadata.isVerified()) { + if (!metadata.isVerifiedSigned()) { // CHECKSTYLE:OFF System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); System.out.println(msg); @@ -178,9 +182,9 @@ public class CanonicalizedDataEncryptionTest { @Test public void noInputEncodingUtf8DataBinarySig() throws PGPException, IOException { String msg = encryptAndSign(message, DocumentSignatureType.BINARY_DOCUMENT, StreamEncoding.UTF8, false); - OpenPgpMetadata metadata = decryptAndVerify(msg); + MessageMetadata metadata = decryptAndVerify(msg); - if (!metadata.isVerified()) { + if (!metadata.isVerifiedSigned()) { // CHECKSTYLE:OFF System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); System.out.println(msg); @@ -192,9 +196,9 @@ public class CanonicalizedDataEncryptionTest { @Test public void noInputEncodingUtf8DataTextSig() throws PGPException, IOException { String msg = encryptAndSign(message, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT, StreamEncoding.UTF8, false); - OpenPgpMetadata metadata = decryptAndVerify(msg); + MessageMetadata metadata = decryptAndVerify(msg); - if (!metadata.isVerified()) { + if (!metadata.isVerifiedSigned()) { // CHECKSTYLE:OFF System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); System.out.println(msg); @@ -207,9 +211,9 @@ public class CanonicalizedDataEncryptionTest { @Test public void inputEncodingBinaryDataBinarySig() throws PGPException, IOException { String msg = encryptAndSign(message, DocumentSignatureType.BINARY_DOCUMENT, StreamEncoding.BINARY, true); - OpenPgpMetadata metadata = decryptAndVerify(msg); + MessageMetadata metadata = decryptAndVerify(msg); - if (!metadata.isVerified()) { + if (!metadata.isVerifiedSigned()) { // CHECKSTYLE:OFF System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); System.out.println(msg); @@ -221,9 +225,9 @@ public class CanonicalizedDataEncryptionTest { @Test public void inputEncodingBinaryDataTextSig() throws PGPException, IOException { String msg = encryptAndSign(message, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT, StreamEncoding.BINARY, true); - OpenPgpMetadata metadata = decryptAndVerify(msg); + MessageMetadata metadata = decryptAndVerify(msg); - if (!metadata.isVerified()) { + if (!metadata.isVerifiedSigned()) { // CHECKSTYLE:OFF System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); System.out.println(msg); @@ -235,9 +239,9 @@ public class CanonicalizedDataEncryptionTest { @Test public void inputEncodingTextDataBinarySig() throws PGPException, IOException { String msg = encryptAndSign(message, DocumentSignatureType.BINARY_DOCUMENT, StreamEncoding.TEXT, true); - OpenPgpMetadata metadata = decryptAndVerify(msg); + MessageMetadata metadata = decryptAndVerify(msg); - if (!metadata.isVerified()) { + if (!metadata.isVerifiedSigned()) { // CHECKSTYLE:OFF System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); System.out.println(msg); @@ -249,9 +253,9 @@ public class CanonicalizedDataEncryptionTest { @Test public void inputEncodingTextDataTextSig() throws PGPException, IOException { String msg = encryptAndSign(message, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT, StreamEncoding.TEXT, true); - OpenPgpMetadata metadata = decryptAndVerify(msg); + MessageMetadata metadata = decryptAndVerify(msg); - if (!metadata.isVerified()) { + if (!metadata.isVerifiedSigned()) { // CHECKSTYLE:OFF System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); System.out.println(msg); @@ -263,9 +267,9 @@ public class CanonicalizedDataEncryptionTest { @Test public void inputEncodingUtf8DataBinarySig() throws PGPException, IOException { String msg = encryptAndSign(message, DocumentSignatureType.BINARY_DOCUMENT, StreamEncoding.UTF8, true); - OpenPgpMetadata metadata = decryptAndVerify(msg); + MessageMetadata metadata = decryptAndVerify(msg); - if (!metadata.isVerified()) { + if (!metadata.isVerifiedSigned()) { // CHECKSTYLE:OFF System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); System.out.println(msg); @@ -277,9 +281,9 @@ public class CanonicalizedDataEncryptionTest { @Test public void inputEncodingUtf8DataTextSig() throws PGPException, IOException { String msg = encryptAndSign(message, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT, StreamEncoding.UTF8, true); - OpenPgpMetadata metadata = decryptAndVerify(msg); + MessageMetadata metadata = decryptAndVerify(msg); - if (!metadata.isVerified()) { + if (!metadata.isVerifiedSigned()) { // CHECKSTYLE:OFF System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); System.out.println(msg); @@ -288,11 +292,9 @@ public class CanonicalizedDataEncryptionTest { } } - @Test - public void resultOfDecryptionIsCRLFEncoded() throws PGPException, IOException { - String before = "Foo\nBar!\n"; - String after = "Foo\r\nBar!\r\n"; - + @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)); @@ -309,6 +311,16 @@ public class CanonicalizedDataEncryptionTest { 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"; @@ -360,7 +372,7 @@ public class CanonicalizedDataEncryptionTest { return msg; } - private OpenPgpMetadata decryptAndVerify(String msg) throws PGPException, IOException { + private MessageMetadata decryptAndVerify(String msg) throws PGPException, IOException { ByteArrayInputStream in = new ByteArrayInputStream(msg.getBytes(StandardCharsets.UTF_8)); DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() .onInputStream(in) @@ -371,7 +383,7 @@ public class CanonicalizedDataEncryptionTest { Streams.drain(decryptionStream); decryptionStream.close(); - return decryptionStream.getResult(); + return decryptionStream.getMetadata(); } @Test @@ -439,8 +451,8 @@ public class CanonicalizedDataEncryptionTest { Streams.pipeAll(decryptionStream, decrypted); decryptionStream.close(); - OpenPgpMetadata metadata = decryptionStream.getResult(); - assertTrue(metadata.isVerified(), "Not verified! Sig Type: " + sigType + " StreamEncoding: " + streamEncoding); + 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 index a53999a6..e5f9e370 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CertificateWithMissingSecretKeyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CertificateWithMissingSecretKeyTest.java @@ -83,7 +83,7 @@ public class CertificateWithMissingSecretKeyTest { encryptionSubkeyId = PGPainless.inspectKeyRing(secretKeys) .getEncryptionSubkeys(EncryptionPurpose.ANY).get(0).getKeyID(); // remove the encryption/decryption secret key - missingDecryptionSecKey = KeyRingUtils.removeSecretKey(secretKeys, encryptionSubkeyId); + missingDecryptionSecKey = KeyRingUtils.stripSecretKey(secretKeys, encryptionSubkeyId); } @Test 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 cabfdbb1..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 @@ -96,11 +96,11 @@ public class CleartextSignatureVerificationTest { Streams.pipeAll(decryptionStream, out); decryptionStream.close(); - OpenPgpMetadata result = decryptionStream.getResult(); - assertTrue(result.isVerified()); - assertTrue(result.isCleartextSigned()); + 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()); @@ -125,10 +125,10 @@ public class CleartextSignatureVerificationTest { 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); @@ -178,7 +178,7 @@ public class CleartextSignatureVerificationTest { Streams.pipeAll(decryptionStream, out); decryptionStream.close(); - OpenPgpMetadata metadata = decryptionStream.getResult(); + MessageMetadata metadata = decryptionStream.getMetadata(); assertEquals(1, metadata.getVerifiedSignatures().size()); } @@ -210,8 +210,8 @@ public class CleartextSignatureVerificationTest { Streams.pipeAll(verificationStream, msgOut); verificationStream.close(); - OpenPgpMetadata metadata = verificationStream.getResult(); - assertTrue(metadata.isVerified()); + MessageMetadata metadata = verificationStream.getMetadata(); + assertTrue(metadata.isVerifiedSigned()); } @Test 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 index 73c3bf56..71fbf9be 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactoryTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactoryTest.java @@ -55,14 +55,14 @@ public class CustomPublicKeyDataDecryptorFactoryTest { HardwareSecurity.DecryptionCallback hardwareDecryptionCallback = new HardwareSecurity.DecryptionCallback() { @Override - public byte[] decryptSessionKey(long keyId, int keyAlgorithm, byte[] sessionKeyData) + 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}); + return internal.recoverSessionData(keyAlgorithm, new byte[][] {sessionKeyData}, pkeskVersion); } catch (PGPException e) { throw new HardwareSecurity.HardwareSecurityException(); } 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 e939de0a..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 @@ -64,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(); @@ -72,17 +72,34 @@ public class DecryptAndVerifyMessageTest { assertArrayEquals(expected, actual); assertTrue(metadata.isEncrypted()); - assertTrue(metadata.isSigned()); - assertFalse(metadata.isCleartextSigned()); - 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 { @@ -104,7 +121,7 @@ public class DecryptAndVerifyMessageTest { 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(); @@ -112,14 +129,13 @@ public class DecryptAndVerifyMessageTest { assertArrayEquals(expected, actual); assertTrue(metadata.isEncrypted()); - assertTrue(metadata.isSigned()); - assertFalse(metadata.isCleartextSigned()); - 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()); } @@ -137,6 +153,6 @@ public class DecryptAndVerifyMessageTest { () -> PGPainless.decryptAndOrVerify() .onInputStream(ciphertextIn) .withOptions(ConsumerOptions.get() - .addDecryptionPassphrase(Passphrase.fromPassword("sw0rdf1sh")))); + .addMessagePassphrase(Passphrase.fromPassword("sw0rdf1sh")))); } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptHiddenRecipientMessageTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptHiddenRecipientMessageTest.java index 5400d17c..4eb7b203 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptHiddenRecipientMessageTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptHiddenRecipientMessageTest.java @@ -139,8 +139,9 @@ public class DecryptHiddenRecipientMessageTest { 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.ANY); 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 index 2b60ea02..2b222c83 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/IgnoreUnknownSignatureVersionsTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/IgnoreUnknownSignatureVersionsTest.java @@ -5,6 +5,7 @@ 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; @@ -71,7 +72,7 @@ public class IgnoreUnknownSignatureVersionsTest { try { cert = PGPainless.readKeyRing().publicKeyRing(CERT); } catch (IOException e) { - + fail("Cannot parse certificate.", e); } } @@ -103,9 +104,9 @@ public class IgnoreUnknownSignatureVersionsTest { "ou1uiXJaDzZ6wQfB\n" + "=uHRc\n" + "-----END PGP SIGNATURE-----\n"; - OpenPgpMetadata metadata = verifySignature(cert, BASE_CASE); + MessageMetadata metadata = verifySignature(cert, BASE_CASE); - assertTrue(metadata.isVerified()); + assertTrue(metadata.isVerifiedSigned()); } @Test @@ -136,9 +137,9 @@ public class IgnoreUnknownSignatureVersionsTest { "ou1uiXJaDzZ6wQfB\n" + "=/JL1\n" + "-----END PGP SIGNATURE-----\n"; - OpenPgpMetadata metadata = verifySignature(cert, SIG4SIG23); + MessageMetadata metadata = verifySignature(cert, SIG4SIG23); - assertTrue(metadata.isVerified()); + assertTrue(metadata.isVerifiedSigned()); } @Test @@ -169,12 +170,12 @@ public class IgnoreUnknownSignatureVersionsTest { "ou1uiXJaDzZ6wQfB\n" + "=Yc8d\n" + "-----END PGP SIGNATURE-----\n"; - OpenPgpMetadata metadata = verifySignature(cert, SIG23SIG4); + MessageMetadata metadata = verifySignature(cert, SIG23SIG4); - assertTrue(metadata.isVerified()); + assertTrue(metadata.isVerifiedSigned()); } - private OpenPgpMetadata verifySignature(PGPPublicKeyRing cert, String BASE_CASE) throws PGPException, IOException { + 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) @@ -183,6 +184,6 @@ public class IgnoreUnknownSignatureVersionsTest { Streams.drain(decryptionStream); decryptionStream.close(); - return decryptionStream.getResult(); + return decryptionStream.getMetadata(); } } 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 index 771cb8f1..7c443829 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MessageMetadataTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MessageMetadataTest.java @@ -24,13 +24,13 @@ 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 encrypted. + // 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.depth + 1); - MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData(SymmetricKeyAlgorithm.AES_128, compressedData.depth + 1); - MessageMetadata.EncryptedData encryptedData1 = new MessageMetadata.EncryptedData(SymmetricKeyAlgorithm.AES_256, encryptedData.depth + 1); + 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); 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 f7d36aba..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; @@ -59,29 +58,6 @@ public class MissingPassphraseForDecryptionTest { message = out.toByteArray(); } - @Test - public void invalidPostponedKeysStrategyTest() { - SecretKeyPassphraseProvider callback = new SecretKeyPassphraseProvider() { - @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 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 index 01966bbe..6102372a 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java @@ -34,6 +34,7 @@ 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; @@ -281,7 +282,7 @@ public class OpenPgpMessageInputStreamTest { EncryptionStream enc = PGPainless.encryptAndOrSign() .onOutputStream(System.out) .withOptions(ProducerOptions.encrypt(EncryptionOptions.get() - .addPassphrase(Passphrase.fromPassword(PASSPHRASE))) + .addMessagePassphrase(Passphrase.fromPassword(PASSPHRASE))) .overrideCompressionAlgorithm(CompressionAlgorithm.UNCOMPRESSED)); enc.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); enc.close(); @@ -408,7 +409,7 @@ public class OpenPgpMessageInputStreamTest { public void testProcessSENC_LIT(Processor processor) throws PGPException, IOException { Tuple result = processor.process(SENC_LIT, ConsumerOptions.get() - .addDecryptionPassphrase(Passphrase.fromPassword(PASSPHRASE))); + .addMessagePassphrase(Passphrase.fromPassword(PASSPHRASE))); String plain = result.getA(); assertEquals(PLAINTEXT, plain); MessageMetadata metadata = result.getB(); @@ -652,6 +653,22 @@ public class OpenPgpMessageInputStreamTest { 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); 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 c86a823c..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 @@ -193,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 index ba80c69d..f06f0233 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PreventDecryptionUsingNonEncryptionKeyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PreventDecryptionUsingNonEncryptionKeyTest.java @@ -14,6 +14,7 @@ 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; @@ -183,12 +184,29 @@ public class PreventDecryptionUsingNonEncryptionKeyTest { Streams.drain(decryptionStream); decryptionStream.close(); - OpenPgpMetadata metadata = decryptionStream.getResult(); + 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); diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/SignedMessageVerificationWithoutCertIsStillSignedTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/SignedMessageVerificationWithoutCertIsStillSignedTest.java index 27bc9954..9f85b241 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/SignedMessageVerificationWithoutCertIsStillSignedTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/SignedMessageVerificationWithoutCertIsStillSignedTest.java @@ -39,10 +39,10 @@ public class SignedMessageVerificationWithoutCertIsStillSignedTest { Streams.pipeAll(verificationStream, out); verificationStream.close(); - OpenPgpMetadata metadata = verificationStream.getResult(); + MessageMetadata metadata = verificationStream.getMetadata(); - assertFalse(metadata.isCleartextSigned()); - 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/VerifyDetachedSignatureTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyDetachedSignatureTest.java index fa1427d3..e1406f87 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyDetachedSignatureTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyDetachedSignatureTest.java @@ -65,8 +65,8 @@ public class VerifyDetachedSignatureTest { Streams.drain(verifier); verifier.close(); - OpenPgpMetadata metadata = verifier.getResult(); - assertTrue(metadata.isVerified()); + MessageMetadata metadata = verifier.getMetadata(); + assertTrue(metadata.isVerifiedSigned()); } @Test @@ -140,7 +140,7 @@ public class VerifyDetachedSignatureTest { Streams.drain(verifier); verifier.close(); - OpenPgpMetadata metadata = verifier.getResult(); - assertTrue(metadata.isVerified()); + 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" + + "lQcYBGYQBhIBEACVXJstPdymc/0ZAQYWSy/hOpHFV0YBgon4ymgIrN0xlJgW0Lju\n" + + "oSW9pHen2MWEdLTUJ1eXrj10QPkB1oFpilzQFqXRYTxjrbeoRZjDXoGEf7JTFcIx\n" + + "R/i385qreJ2ZYw9pUuBCW2R3juiUwzwiwC7/2y2qADOh9TPnJyyoPv3oDuGdLd7h\n" + + "ge3loYOySF0d0JZzFr+x3yQNDWUibiJbckZ1jFpLV2oyHNV9lpxH22xW/nhmanqR\n" + + "vFe6PZCK4UNQBuqY9pvwp7neoM4j5h862LSuEmG4LjIaYp2DBf31jIsWYk7cCgcP\n" + + "p+t6/9/AGwA05n36G2ncnrcrNX25R1F8b0tqZgEe/fOJ4dNCSTeV4pqSotxxeKFx\n" + + "dqY6kvOCCauS5ZLjmMUimgOkDk7/3ePEcRoUMpbhETs3hOcfMMLsh81OL8FdvZVc\n" + + "xxoYmT0ca2kDwixSQ+lLfDISECIEW080H7J/bez3wSjYLOfypNIGCdrZ+9zwtHYZ\n" + + "QxmziDnjprkDmTXKYA1KYvPXhfxHBpmn8spI1MJawHhF7zI9pycWvWc+oty6+/B+\n" + + "dVrx9G9TNDl4xDctehCXC9LErVUW4cAJxZlft1dFc/49JpSRPWzAcw8Q5XOZb2yJ\n" + + "qjUnzw690T0COD5CCM34Fh9/ZcDZXBF/WJT9lWMDFHq84fW2tP+FLAd68QARAQAB\n" + + "AA/7BdEpGGZgyF+bRQ5tjQ/sk3PTTY4/Y1VELlsjsukyMpLvWwqFL7YGLd0D7ZbB\n" + + "sPQipS7+RSGiGK79g7Bo/3h5P0yo58vgedLzZJHuIZD14yuRNGJg9JTK/2ioM3MF\n" + + "IEr9XkRXof33noU2uOR83EjQpWJR5JCrC7m5bCoLngA/pa2lF8wIWPWiE3lqeaN1\n" + + "c1dpiwRUMiDXDjMUjamGz2zwVY+omWdg8wawdRQY9HhIcrLo2YRE6Pwe2cAGd+ib\n" + + "oh6pPSZA0h4vw2RPFixv0IjnSiHcVtUyjYu3R4aqN2MvEsLnACbb04sReFh5MsnA\n" + + "0YHQkjcTGzNYCuf20SQRhl+HRLq2xJnKzZsjYjElbnOuEDuyeMAgHs7/CO7+72t3\n" + + "LCjrEX6JYi1tuNdbu43BFn9TM2aH5ix+XK+/3ZKgmM3fNaLZxM8041MtGtdWTNnp\n" + + "gySKhyLChC/2R0p2MY6fAzRi045ED96fSaKpfroHn0Lw09rNlvEwD1rGHQyGHl0c\n" + + "rBWHO89Gpfx6lBOuyC+kJG0Cm5BJkXHpFJ8ky+xoEDzHUgNdXsAvEyj2Dqxkzojz\n" + + "oC9M7vS66p45wXXsbEcGCG/FCw8XRNaeOUKOBgQ19IdTsoqCPBfcqWYMkqqcmcwF\n" + + "62eT/A8AyHrAi+srJawQCAQtrWnQ9A/oOapqPyUhH/QdjAkIALvoejVUd8OwUkHt\n" + + "n/C7mgnoAlPBPPlQrCYKR4bZdCkleWjaz8zUXRKSfTWn1rXDHjDx7SR7kykBqLFZ\n" + + "tzkXx/dzB6mOS8Mw8t25Asn9kocMlYQebLDf2jw1HjeC9q8+SY2Cub3YQuVJdZI5\n" + + "eu0Bo4DgMn1EmQPuJSY2DO6tHu/kyTT35oJfJQ5ntYPUSWMLBWYV2tfcHx3UkaQt\n" + + "nXTuiL7IfEeRniv1I6b2xY5So+WbPuSQjSbmqGfGHythzNmAZsWmCXFZFHQqnbHn\n" + + "DtZlqzjIJmaJK6qNLCcUC1ZC6GPU7/n09Q/n/Edu5uMi3Ktc+50CPzjs8x7q6ipb\n" + + "e03Hqh8IAMt8VAIC6EeFN2MoyExT95QR4HXzDC2iHFLYxzVpaBrTJ9RlVlStTe+a\n" + + "W5euSHCWhGA8Nh8+s4NHLkYk3LezC89+japsooVU/F2QHfEuHWBtevw04+vub8KG\n" + + "eRg5+7RK0DnCK3MM/yi7/06J+JTV9rAe3qyaCX2mGQK8QeoC3ea6Gk/d8HY/q0y6\n" + + "LFP/0PIiyA55nYdZQoSEFEfmg1lJIU0h9D+FaoiVYjd1IVnHlcooFReJ+SudY1rV\n" + + "UP4mckKlgbyfOmB1G8fehTGohfRCWCx92pKKk9jxsLcY8jUJpO7Utzg00OwJWRop\n" + + "QyajNCChzXcJju85OEQS8noBIGk7WO8IAJude9ei9+M4sKGUN42aRcX2njLggajH\n" + + "0BoqcCP6WJGZ+08kZY+PhvUWAloFm/icOtWRJJbgjvVKZU+yoFjdPu0KIDCYp7pZ\n" + + "3SzUoZeY8tt1dNCZfcsJB+WeYW3HQXAavUP4+gR8ro7WjNQhvWddLgHo1z+rNoti\n" + + "1BUFlQkVKdreM9ll3HUpYy2xcKMylOxMeL/qEbRCO5L7hDGbjLrio3ynpHrVLIi2\n" + + "+wWU+JB2pVQnI4+tm2gMAil63wu8WJZz8BVxn3AhAQgQIH9OfuRCpPwvrBDIAyog\n" + + "T8razUcCHQtulj9Pchu74isreAwMn1Z4Ddol89ANfIKn14mla1WaGx5wiLQcQ29t\n" + + "cGxleCBSU0EgPHRlc3RAdGVzdC50ZXN0PokCUwQTAQoARwUCZhAGFQkQr+RnsppB\n" + + "oM4WIQSs/H/6Ar7B6FACLTWv5GeymkGgzgKeAQKbAQUWAgMBAAQLCQgHBRUKCQgL\n" + + "BYkJZgGAApkBAADrLw//U5KEArVsiOmsK02Fru9BAAbFf2+otw55d5UFNCd/G93T\n" + + "n38lTuThl9Tk3dLGd/tL39fZjB7XlJ2eBcV/8uzBp9F3d14j/GDwkx7gkaA34TMr\n" + + "g73XnIdw2V88WEuKhFg0JAGUm6C8LEtlHYie7kS+gDtyQjQw7qUGhCG/QHyIi0iO\n" + + "DA31NCOSHJI2rhK8nkS6SGGzUDwWEP92bKnlcqtooCwdEdPuRuNCJ6J5GDdhX06t\n" + + "ZngnLRMHt6pN/UDYdNNVJaIV0ZFICMRTPJSSHzIV59cQx20DBZ7cYK2ag6uDx+O+\n" + + "tVWamNcb9JR5TuN7PX+Q/EEhiKpaom/lFNcQwqj4kclwraZXQ2HaHDoJoqCIaBVA\n" + + "4eGG1nCy2weEgrSPk4GxFjqAiaifR3Y75JqPG8VTpS51iKU4gvs4EeU/8+WFBOjp\n" + + "lZH7FAS9vqCPltZm/6hTD7phqRXAZe3J1RoxFFAl3ikzz1Wz7y/m/y4ouz3bIgRR\n" + + "gjFmjPYSbh/p4SMj1jELebs7klqp7hKsXFP+mZm/Oh8WK96G6IIt/TwWJ6exOB1Z\n" + + "pcS9RiFOGjfpIy0xzMOGj9EFX3qs+jwpc+oRRCAyqRZZNrvRCgY4crHVBQa5AyJb\n" + + "TJ4OfofY8Lj+MBNJO5M3FpljnjP89JMxFqtYGzj/qtDv9QFWXan2zbMRfb8q2kWd\n" + + "BxgEZhAGFQEQAPqHNmUHC6rw1y/1uXyd0Y7NoprEB2TAWoUxbX3ZFfUCGh881CAU\n" + + "8JQiTED5yXZRbwi4JQSBXg+yRjx8puB5AvHAvZn2AWreXaTyDfoXMXg20qm5sp2V\n" + + "mVmtr6iI5rXifa0I9kMJvW0jNNsPFgXuo3/1dTM7U11/HDzdmh9arKGB7MnQphmV\n" + + "T3L0wFhY9lHMtNn3CmiTqJNDJhHWTMTWeOictqULwIccFoQJEdBzhJZE3+KX+yv/\n" + + "a74DJoSa27SQjJEGUmXCEx3GZiwwGP201hP5TKPLLxfd5B+W+uGbPP/T9O3LEDNp\n" + + "hUXYmuKSp77+Zq0JHWnvlSvKDr4oDQ8Wgiq1iDD1baYY1EWmn5olVwyi7jS2m5mG\n" + + "fMW7MFBssG1nbmUama/pPLqcV4nr6URveGDFwcx6/ulMkN9P0C1qR3K4Asx1ZB49\n" + + "Kt+iz1fuzh+lFU35DS/wRT9LyUzaSvaGegThHNAhw24m19vb5mrBUtQOGuW0MCEh\n" + + "CzWkhjMaQVRbCrUqT+ab+X/2xA7ETKITtq40IAsk3tW8YLnKfEt+u7BMMGqPJV9D\n" + + "oVRQZwW+xc47T6KfmNEw2RzkoxbmZMnSUBjp0MFWs6Tc9a0OMqnwdbrsxlN1AP0e\n" + + "XhpRs8Hl2TuloY9j1yDW2aZ8l1g0KQMbkPKH9dgf+XR0+un1p6HsJpRHABEBAAEA\n" + + "D/wKn48r64eQIRRO4VGTOjH3pzqc63EQ0aNFAJqO+pSWxhcLeg3YqmqlLWskWjMz\n" + + "xDI8IWrYbQ/rBHk7+WEuJZN9YtnnXGok+PbplqYHE9KyMUjvj4NGcWCGT/oh4GRA\n" + + "FDGWE8o1f4U7yoFkRJh/eeYO9/6XRI29ajVtU0xExhiJ5LOAv0s7zHwI+N3rISKY\n" + + "x2Bn2bTkSFaen/tOSFMLCbkoy/RmvT/VutgtkyDhQPS/Vn5T4nPxIqyT6xhICTUF\n" + + "zBdZ0vXNgNREr/QHLabxoyhswmaAj44Yqf0RZdqPlICarIc3SiQOugu/sXan4uYg\n" + + "EDOUZM2Nf25I5BGJ+LLND/xG89865BzCiCLtIii6HnB6fabbKpPIEm6JTL8NuS7L\n" + + "rjcWefeWWSlrAOjZGC0OySxIVRLWnE3Mw0YLhWdqdb/zit9dP0tYrezdsRmX6I9D\n" + + "eRpGHKhFPLwyuv9Q/7opJqBuj8OmuFBQxNOipr9IKF2OJkqLBcy92rThIETliW5G\n" + + "6xF7wVYe4leEGzYrp4Zi3meO+CJoyw2vVj7RcZKU9Lyc3MR5VxHjl7aqrfhgtpGS\n" + + "3YEmW0O58guXc9hdrVE/dy7r0pW6CZo08w+dv2OSOyvjTdq8SkdE8cKJ52eipR/3\n" + + "SbNIu3sgd3+keXqvXvvhIHjvbqoV/c4cMnzj4FaR5w1xuQgA+18DlzS2+BocmotZ\n" + + "4uhQPheFrQsmInawLOVDVjMIf1si64DrjeKC9+3SjJseTD3nIy26/s9kOI4ixkjb\n" + + "jO4J2/fNxyT5AK6Owcpy8wTOkXaI8MAhPq1IWq8dAZFnWxJNOheNx5HJwhx4Dvrn\n" + + "z4ONDFfKBZ6eSSiak2eJ7B07jjyU1yu1gAXRjc61cZKK9V1dY6/HgJzuzEXLYqpD\n" + + "MXHz33Uqkg1qRtkqECx2i7Vo78gZcASB8fAsE7Rinub9dJlfdwWGMgwOU88g/aMs\n" + + "KaizN4fosqpX+Y+0uofl40lpKQFcmJOMCxCKZeaBD1+UOu2jIgI4UcRfh75FyQn8\n" + + "zzmCZQgA/yQ5fwyQtwYiUIlmwAdKzHDoknVmYdlWwBjlFhLTFJccIjGzzGePN7nE\n" + + "iwxJfX/LZ4ObprT+q0nfIf37cPqXfPv35S/yutLCX5CjkIsiD93aCsDE58/WlwFQ\n" + + "oxi62pAMYl83HfHLtyRFFuXmpnt+Su7tlzEYCRn8JakKy/VzyEEbjRiQgJoB/Mje\n" + + "GGlbfY+huTCGM40gtAduOzt15Qxr4JY1QLArX+Dosf2ylAoXcmcHz9wNIAAb40fr\n" + + "fO13k5FqXQDJh5GFSOQliQX98D5ip1SDK2Ut8EDuq2NPjMbfI08vf9TELhEmTXy2\n" + + "7rCM99in9kFQCkKH7cgTnihyY9N7OwgAmpnlXADaiCQGB7V1QQWhiJCPPB0ptFQi\n" + + "ZxFDgJ8cqX46Wg7cK9pC8uLpYMpTGIGQonygKEOVf5QY3CP4mZ2WujCQ7m4sDpw0\n" + + "tkpdjTkhz4Kz1xw07toxKiqSjwlKq7TLm/HbqNZLijLLyvjTB1xxlwh7XCAaXzr2\n" + + "Ri4dBHVqQIO/sygBVSRPHVqT6nKjo+Bz+qb7Bef71jkANrmIRjzq66X1fYlrQejd\n" + + "4W7/+YNzOsKHcgBoF6A1texG59JH1raD5GVnBfFLWoYx4T57oN0mK2+BDSNCQSYe\n" + + "/gzxxdbocaWMjW71JBudLKh9vXwhqPLD5828YkTJWCyxIAjk/6BtSXxZiQRSBBgB\n" + + "CgI8BQJmEAYYAp4BApsCBRYCAwEABAsJCAcFFQoJCAvBXSAEGQEKAAYFAmYQBhgA\n" + + "CgkQiw37fiqkMiCWGxAA99f7sR1epBqiq1oYVOQoj6ZK/Vzstbv31vx1evQSy4/j\n" + + "9RhD8pNZWg9Kb2U5vv8ZZoJGBNm071P9/sZE5YJm/2GB7CStC3z5WvHPQZK5a2L5\n" + + "d8jp99+fkK87qlWbig4AiRD/nhaoHshsKqp9q+5NEypZapZleQ2HIJ+wW3aqjtj+\n" + + "U54JZiXxd95pMbx6JMee6SvpKGZTceem7jOljFwMZ0I+qPmaFpALJfZI3pxKCozg\n" + + "72yABVk4ICWJ5xZuxfUvoIkCQ9wcw+D3xYaHWQ1jl8l/mzZaBefa4ZlSk//ajgPY\n" + + "HhDrEmhKTQ6Lv0aLC83pVo66IRDZwCrcYFy2cEefxX3FHqGr6sD/cPMMlu/aBzG+\n" + + "oAvx3Xseav9zv5eZlRET4MD8QO7bPC7DeGdBPOhKIAoiFCOB9hIlr7MTNFpQkpcn\n" + + "dBovK3s21+E2cAzPuhrBuHKPXeGV8bHPdmtZk5wtaBfBwwbtAzirGQOx/aimDK2y\n" + + "Tx1Kjyqb6QCdjJopzLLtBGd9PQcpUPenngZ8+8uE2inNlZRczEfB6YtNitAtIx/G\n" + + "qkD6DagReyD/gmqQKUGn/6amYkux9dAs4sD/F1NN1hN4BWlvhpXVkiqG9/1MAu77\n" + + "n+ne74CJcWJ93fKokMvubyusVJfZfuQuLYz0NcwxN3YMlt8yDL4l0ZK+GEjXd8oA\n" + + "CgkQr+RnsppBoM7aUQ//fnTz/4jFGqHssqp3ZQro8Ie4NEmtmjioFzq9FZQX5KAZ\n" + + "uL8q6pT1ChV6uqvL9YgfYgbSGaWaVRIJlt+cfz8EfbHpgHvEj1R94TudE0MajDdR\n" + + "1V4jpEIHtlftoN9m9n60woAFScN+7LjQ/TRZDf2Ie6lBkpTEHr1gUvb/VzyiOSxg\n" + + "sYMbcPPpcymCPJKyzx2DHFryHRS7YzoRHb8Apmlat8ceervNTPzErznsN1LljEA7\n" + + "qhghWgkCrpApTGOESpwcoGli6m62tDZvLzpIJhHK3yan6nC7VcQ09FHdMJc+762Q\n" + + "ZxPbtZalnCrxrh22F1KcJwMYBo+PeOXueL3fCiPGImEe2DT1SIV8wmO5yCffxGoF\n" + + "ylGotT6HrGQPseB8yo0WycVs9PIhyLPc1D6SVerLQn34ru0DxuXX2P6x0UvXxH74\n" + + "z2VCGj78oBv2lguy47d3IEllWhFTJUHyH+KR7gUQlH4f4S0/drqH0s9oLl/xGUET\n" + + "9IHDK/aPh57DTdpyNur/75cty8f94ScmwclYB7L+z1wMMDe3qA9GhVG9UvL1s8oz\n" + + "OQ4T1XwKMf5a4obCkOoyV7B41RdwaKHYVkDGcqvVbQJRQGpyE8fzqFakjjdVN0SN\n" + + "diyJvSrr60QeUdnykTmQRZK+hajAz0hKSHT9L9bhgwfMUDd8SwM8D/shHgvVffGd\n" + + "BxgEZhAGGAEQALcxdafRMdeNOF21Z6AFLIJ4jcsPcgsJ1sTpMHHJCGBfHB5iE5VF\n" + + "LbCYKMES1mhe58JsY2KLDj/9YgzJKfO4tn9SXVWmMCOsVoa9N0OH0H2/QxcipjCt\n" + + "LNFg1nOYVSq47TmgAMC+NnAMPYBh0MlZrr++Z4WFxNxKukNmvlO2JGRTMm+p/6qr\n" + + "zhXyBaVxI2ZW6wqg79JAFKfC7v2b8Zfp7t2ehlRQ0gweHLOFBjIhaZatk93J9pF4\n" + + "epzI6CuBLMwoFKmj/bVbrZPPUabe8vMJt/sZjxQzzfKLnHVRfihs7InFGaf6wgxF\n" + + "7cDDA5/x0/wsmOlOaxyt5nInJnMLdUzmIBVu95YjAaBFejF8t9JzhOgDXQ/A7Zx0\n" + + "EQHDF56FYvIcHVjUCJ4qeLJYCWMNAHBkUbyCltMJnFB+aYc28eOQW6fHBczo+jmr\n" + + "HNjsg+1RWal1ZijzAmCN1aAHY9DPvUZklLn/qpk9EtpI6hLRP94sbbr1bJzibBi9\n" + + "mEvsL8TwhpEqwc+HIGqnvVQHqb1mVEmcGVdhiqmNFe94lyYkv7BWim5mu6b62Dkl\n" + + "wTAcpAowYzqkZ9UXqBlHMGqrFqtSvT6PBh+s7ZCd1/ueACV5bM9G9lU5DRZOcuAS\n" + + "dM0TI6a04PEziV1npOFxcnx+sZApDZ8Rqpa7nkZXsKmxhRoupUi4neAFABEBAAEA\n" + + "D/4//TrHv77VODL0KKVls+j0Of/tahu/11P5vCp71GjkoNRFmKSWg2+OO9ggeOAD\n" + + "3QK/WvTsOv5jQ7K4HJxW0bKNjsujW0V9cHlY30cqg4pEIkbhEe1TG2qISHcgMZmu\n" + + "LqJOeqFIsih5wwzIh2JSsszjlTK75Rn6iO+/E2hv/TOBB76aWps/lnuKFtv6Cib/\n" + + "XGUFdWnP2ypb3y9zzsD4+3HAX9s0IHb+XJZR7qlXYWxsgX0g/6bs8VSC53qRl7F6\n" + + "LpXpG6tHahqbgtNWopHiawak4yyjNeU+T537LNgQbtvA0+Q+VMzrVJHTv0rI18Pg\n" + + "VgOjmwy3G9dfEGXR0bLLhaa2vfvBA54yXTfIN8qn6iJctxRucfJEEYWWIcdXQNmv\n" + + "aD2Ozz0IzDDSMyyZiAibhuoOOL4izinFX9O7AxVEkIf3+IHloxjuT6LSllNOO8ZB\n" + + "xKsyeLFgIjQFXjY4sR+JleML+YkCq46NdHVmbb6TryDelkduqCHOk2PGvdv/1zrf\n" + + "yTRMWQLx80Urd4rOKxNOnqwblj6yt1eOINXA31GkJSIEOkkLdFOZ/H7zp+UqbGE/\n" + + "4nGh/JzBur/jTLIx9ElsrV3lLGEtUa47mgbMYAXIIL2RR+LMlT4JoO3SQXEuvkok\n" + + "RT7NpyoVTWHYlQiR0XruCt4bvWaEVCdiNj565yI7pqPzkQgAxZ5tKTPW+8uHOQSh\n" + + "NuY9Ok2bUcaNvzboOu7t3c9rETf5/XBAjcmjNSxlDLLiZv9Qp3DhcnXu45ZDSh86\n" + + "Sd+o9NlQLTAs/J98rRExDISj42V3q740Hf8jXS3lz4fwlr3YHeV0c1A+YsIoY5nb\n" + + "Bs2UHO5nX1PgtE17Bgbt1+Y3jamK/2+W7WDAiIq7G4uRpeORqrXsjWZDuIaKR03C\n" + + "lxj1c7LqkFUjVpY47mdlBDuXmra7slcYVdabR5oULpRvloAjp+VX3Yfwzl0/I0o6\n" + + "/3WZTRhM4PhkP4imkveykv/BkIDFWfho3pJScru8Aqn3XbccrZO8WY6nONzvYIr+\n" + + "COgWpwgA7VALxeELI7nJiWeZUZ5HBHOMKQFTdioKD9BgwvEPDOXq+VqAT2n3basQ\n" + + "ShYjCot8gnxe3ei+/KQs/a8DkZVDY7XKMqhZyvidxF6weFAbO4Z+sB6LfyJmz5D1\n" + + "nA0iv8laP6at1wgn1MSfpXZm1cXuUHDpyTqWXLxG1qbUpQJS27fl6F0ws1k8Qvno\n" + + "PLjGbsbEeujbWU2lqZDs3L72e/arIpHdO94LNOwSKrGjO1KTkGgue+uGm1u17gEZ\n" + + "3U+rbCvG2lTq7kHZEQHiGaIygxyDJAyIvroPZ9c5nABLGMoH93Pq9tGX2OPxyWLx\n" + + "P1db87EzKRQjFGmZSeeKENRvLYAVcwgAqZTCPD9CkUG6O//rZI/1pPArhN8CY/+x\n" + + "8L2jksvaTLimdBzXyVXlNAPIynMih/QaxNq+WNOmnihVFAw7ur/nmN9Oea33Ue7v\n" + + "6Ryrz9nzi6GonLz0/grzB5XWAiO3+i9WUdVpeTgjlFPXSJsbOjEVRAB2W0heioeM\n" + + "M6weKP8i2O+GF3ULoWzmam4EgsanaSCXgDq1RYXyJhfUYTikVqrD+qJYT0asxU0H\n" + + "S92KmdZ6UKLOTPZdrIL5X0Cj/ejBX/94xHLM0GfXT9CIHTVIVSa2poSbWhN0O+yT\n" + + "NMySr7OHGDUjEUqBL2O5Wm3oyMK/5EoSQ9YJBCrkbkymvoahjrooXZSeiQIzBBgB\n" + + "CgAdBQJmEAYYAp4BApsMBRYCAwEABAsJCAcFFQoJCAsACgkQr+RnsppBoM7gBQ//\n" + + "YGQ6suANXV0C7TvofcMwubahLriBiWGAkI+pNQ7Tmzgql4ALcf4BFsDffcTCy3ue\n" + + "dLAZkEEwT86Ip1qW7mKCN1tScj0g8uU8U78oVShqoyEq0ebemaVmY7gvFWCpGXgr\n" + + "JgNjL4QzRbwzKUdvIkTMmUDiPqhmGuCFV76pazzTmhI/RAzVfnroG0kVDVwODlCI\n" + + "CcswoEOI32v+B8SJbbsXXQ4E6jrDwPIUV/z54VERbi+JcKaes0a/rOLF0FOnoTFs\n" + + "2b2aqt+Knj/RQCP9r3UNl06dONBMaydkhFmMjRjenQb6p+91JyNkXJ6tgD4CDbps\n" + + "2kqryCpZf39E0QU75Rvuofc8vhSRbRvSl7D+NhOdk9c6EEOJPbtozaG+/l8g1K42\n" + + "aMT+TvccBPK0b4EcZtAyCLjDg0eA/GN5+DIhBQCQA4y4KdEf3IMoS+BeEPBNfNyG\n" + + "isVD/f/R/68uGg6S9sKFXOCAXO0rnVIu1Oe123l09FsXVZpFDV3PMbpk7Sxh/20L\n" + + "ERTp5jBV+2J2szmYYxcSfVl6h+P3k8Y8l4evEoPPL6Skz9uZI5C3UB1c6dnWBcRm\n" + + "G759QEMpJuvyuwFZkcoGxfVvtneZsTGajEnNOB4o46hb9a5DLJ9tc06zG+j+eUeR\n" + + "C6mwvn/JYRaGr9uG4zpdxNeFmQ80yitmGGllHenjfaU=\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 index 2a12e74a..6b9d9cab 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyVersion3SignaturePacketTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyVersion3SignaturePacketTest.java @@ -43,8 +43,8 @@ class VerifyVersion3SignaturePacketTest { .onInputStream(new ByteArrayInputStream(DATA)) .withOptions(options); - OpenPgpMetadata metadata = processSignedData(verifier); - assertTrue(metadata.containsVerifiedSignatureFrom(TestKeys.getEmilPublicKeyRing())); + MessageMetadata metadata = processSignedData(verifier); + assertTrue(metadata.isVerifiedSignedBy(TestKeys.getEmilPublicKeyRing())); } private static PGPSignature generateV3Signature() throws IOException, PGPException { @@ -61,9 +61,9 @@ class VerifyVersion3SignaturePacketTest { return signatureGenerator.generate(); } - 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/VerifyWithMissingPublicKeyCallbackTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyWithMissingPublicKeyCallbackTest.java index 14136650..0d58e7dd 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyWithMissingPublicKeyCallbackTest.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; @@ -63,10 +61,9 @@ public class VerifyWithMissingPublicKeyCallbackTest { .withOptions(new ConsumerOptions() .addVerificationCert(unrelatedKeys) .setMissingCertificateCallback(new MissingPublicKeyCallback() { - @Nullable @Override - public PGPPublicKeyRing onMissingPublicKeyEncountered(@Nonnull Long keyId) { - assertEquals(signingKey.getKeyID(), (long) keyId, "Signing key-ID mismatch."); + public PGPPublicKeyRing onMissingPublicKeyEncountered(long keyId) { + assertEquals(signingKey.getKeyID(), keyId, "Signing key-ID mismatch."); return signingPubKeys; } })); @@ -76,7 +73,7 @@ public class VerifyWithMissingPublicKeyCallbackTest { 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 index b3ecabc8..f3336373 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/WrongSignerUserIdTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/WrongSignerUserIdTest.java @@ -102,12 +102,12 @@ public class WrongSignerUserIdTest { Streams.pipeAll(decryptionStream, out); decryptionStream.close(); - OpenPgpMetadata metadata = decryptionStream.getResult(); + MessageMetadata metadata = decryptionStream.getMetadata(); if (expectSuccessfulVerification) { - assertTrue(metadata.isVerified()); + assertTrue(metadata.isVerifiedSigned()); } else { - assertFalse(metadata.isVerified()); + assertFalse(metadata.isVerifiedSigned()); } } 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 index d0486a3e..10f8dceb 100644 --- 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 @@ -21,8 +21,8 @@ public class PDATest { @Test public void testSimpleLiteralMessageIsValid() throws MalformedOpenPgpMessageException { PDA check = new PDA(); - check.next(InputSymbol.LiteralData); - check.next(InputSymbol.EndOfSequence); + check.next(InputSymbol.LITERAL_DATA); + check.next(InputSymbol.END_OF_SEQUENCE); assertTrue(check.isValid()); } @@ -35,10 +35,10 @@ public class PDATest { @Test public void testSimpleOpsSignedMesssageIsValid() throws MalformedOpenPgpMessageException { PDA check = new PDA(); - check.next(InputSymbol.OnePassSignature); - check.next(InputSymbol.LiteralData); - check.next(InputSymbol.Signature); - check.next(InputSymbol.EndOfSequence); + check.next(InputSymbol.ONE_PASS_SIGNATURE); + check.next(InputSymbol.LITERAL_DATA); + check.next(InputSymbol.SIGNATURE); + check.next(InputSymbol.END_OF_SEQUENCE); assertTrue(check.isValid()); } @@ -52,9 +52,9 @@ public class PDATest { @Test public void testSimplePrependSignedMessageIsValid() throws MalformedOpenPgpMessageException { PDA check = new PDA(); - check.next(InputSymbol.Signature); - check.next(InputSymbol.LiteralData); - check.next(InputSymbol.EndOfSequence); + check.next(InputSymbol.SIGNATURE); + check.next(InputSymbol.LITERAL_DATA); + check.next(InputSymbol.END_OF_SEQUENCE); assertTrue(check.isValid()); } @@ -68,11 +68,11 @@ public class PDATest { @Test public void testOPSSignedCompressedMessageIsValid() throws MalformedOpenPgpMessageException { PDA check = new PDA(); - check.next(InputSymbol.OnePassSignature); - check.next(InputSymbol.CompressedData); + 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.EndOfSequence); + check.next(InputSymbol.SIGNATURE); + check.next(InputSymbol.END_OF_SEQUENCE); assertTrue(check.isValid()); } @@ -80,105 +80,105 @@ public class PDATest { @Test public void testOPSSignedEncryptedMessageIsValid() { PDA check = new PDA(); - check.next(InputSymbol.OnePassSignature); - check.next(InputSymbol.EncryptedData); - check.next(InputSymbol.Signature); - check.next(InputSymbol.EndOfSequence); + 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.LiteralData); - check.next(InputSymbol.EndOfSequence); + check.next(InputSymbol.LITERAL_DATA); + check.next(InputSymbol.END_OF_SEQUENCE); assertThrows(MalformedOpenPgpMessageException.class, - () -> check.next(InputSymbol.Signature)); + () -> check.next(InputSymbol.SIGNATURE)); } @Test public void testEncryptedMessageWithAppendedStandaloneSigIsNotValid() { PDA check = new PDA(); - check.next(InputSymbol.EncryptedData); + check.next(InputSymbol.ENCRYPTED_DATA); assertThrows(MalformedOpenPgpMessageException.class, - () -> check.next(InputSymbol.Signature)); + () -> check.next(InputSymbol.SIGNATURE)); } @Test public void testOPSSignedEncryptedMessageWithMissingSigIsNotValid() { PDA check = new PDA(); - check.next(InputSymbol.OnePassSignature); - check.next(InputSymbol.EncryptedData); + check.next(InputSymbol.ONE_PASS_SIGNATURE); + check.next(InputSymbol.ENCRYPTED_DATA); assertThrows(MalformedOpenPgpMessageException.class, - () -> check.next(InputSymbol.EndOfSequence)); + () -> check.next(InputSymbol.END_OF_SEQUENCE)); } @Test public void testTwoLiteralDataIsNotValid() { PDA check = new PDA(); - check.next(InputSymbol.LiteralData); + check.next(InputSymbol.LITERAL_DATA); assertThrows(MalformedOpenPgpMessageException.class, - () -> check.next(InputSymbol.LiteralData)); + () -> check.next(InputSymbol.LITERAL_DATA)); } @Test public void testTrailingSigIsNotValid() { PDA check = new PDA(); - check.next(InputSymbol.LiteralData); + check.next(InputSymbol.LITERAL_DATA); assertThrows(MalformedOpenPgpMessageException.class, - () -> check.next(InputSymbol.Signature)); + () -> check.next(InputSymbol.SIGNATURE)); } @Test public void testOPSAloneIsNotValid() { PDA check = new PDA(); - check.next(InputSymbol.OnePassSignature); + check.next(InputSymbol.ONE_PASS_SIGNATURE); assertThrows(MalformedOpenPgpMessageException.class, - () -> check.next(InputSymbol.EndOfSequence)); + () -> check.next(InputSymbol.END_OF_SEQUENCE)); } @Test public void testOPSLitWithMissingSigIsNotValid() { PDA check = new PDA(); - check.next(InputSymbol.OnePassSignature); - check.next(InputSymbol.LiteralData); + check.next(InputSymbol.ONE_PASS_SIGNATURE); + check.next(InputSymbol.LITERAL_DATA); assertThrows(MalformedOpenPgpMessageException.class, - () -> check.next(InputSymbol.EndOfSequence)); + () -> check.next(InputSymbol.END_OF_SEQUENCE)); } @Test public void testCompressedMessageWithStandalongAppendedSigIsNotValid() { PDA check = new PDA(); - check.next(InputSymbol.CompressedData); + check.next(InputSymbol.COMPRESSED_DATA); assertThrows(MalformedOpenPgpMessageException.class, - () -> check.next(InputSymbol.Signature)); + () -> check.next(InputSymbol.SIGNATURE)); } @Test public void testOPSCompressedDataWithMissingSigIsNotValid() { PDA check = new PDA(); - check.next(InputSymbol.OnePassSignature); - check.next(InputSymbol.CompressedData); + check.next(InputSymbol.ONE_PASS_SIGNATURE); + check.next(InputSymbol.COMPRESSED_DATA); assertThrows(MalformedOpenPgpMessageException.class, - () -> check.next(InputSymbol.EndOfSequence)); + () -> check.next(InputSymbol.END_OF_SEQUENCE)); } @Test public void testCompressedMessageFollowedByTrailingLiteralDataIsNotValid() { PDA check = new PDA(); - check.next(InputSymbol.CompressedData); + check.next(InputSymbol.COMPRESSED_DATA); assertThrows(MalformedOpenPgpMessageException.class, - () -> check.next(InputSymbol.LiteralData)); + () -> check.next(InputSymbol.LITERAL_DATA)); } @Test public void testOPSWithPrependedSigIsValid() { PDA check = new PDA(); - check.next(InputSymbol.Signature); - check.next(InputSymbol.OnePassSignature); - check.next(InputSymbol.LiteralData); - check.next(InputSymbol.Signature); - check.next(InputSymbol.EndOfSequence); + 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()); } @@ -186,11 +186,11 @@ public class PDATest { @Test public void testPrependedSigInsideOPSSignedMessageIsValid() { PDA check = new PDA(); - check.next(InputSymbol.OnePassSignature); - check.next(InputSymbol.Signature); - check.next(InputSymbol.LiteralData); - check.next(InputSymbol.Signature); - check.next(InputSymbol.EndOfSequence); + 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 index 50b7cbf7..1346e6b7 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/BcHashContextSignerTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/BcHashContextSignerTest.java @@ -27,7 +27,7 @@ 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.OpenPgpMetadata; +import org.pgpainless.decryption_verification.MessageMetadata; import org.pgpainless.key.generation.type.rsa.RsaLength; import org.pgpainless.key.protection.SecretKeyRingProtector; @@ -105,8 +105,8 @@ public class BcHashContextSignerTest { Streams.pipeAll(decryptionStream, out); decryptionStream.close(); - OpenPgpMetadata metadata = decryptionStream.getResult(); - assertTrue(metadata.isVerified()); + MessageMetadata metadata = decryptionStream.getMetadata(); + assertTrue(metadata.isVerifiedSigned()); } private PGPSignature signMessage(byte[] message, HashAlgorithm hashAlgorithm, PGPSecretKeyRing secretKeys) 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 216d0c65..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 @@ -33,7 +33,7 @@ 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.decryption_verification.MessageMetadata; import org.pgpainless.exception.KeyException; import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.key.TestKeys; @@ -185,11 +185,10 @@ 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()); } @TestTemplate @@ -233,7 +232,7 @@ public class EncryptDecryptTest { Streams.pipeAll(verifier, dummyOut); verifier.close(); - OpenPgpMetadata decryptionResult = verifier.getResult(); + MessageMetadata decryptionResult = verifier.getMetadata(); assertFalse(decryptionResult.getVerifiedSignatures().isEmpty()); } @@ -263,7 +262,7 @@ public class EncryptDecryptTest { Streams.pipeAll(verifier, signOut); verifier.close(); - OpenPgpMetadata metadata = verifier.getResult(); + MessageMetadata metadata = verifier.getMetadata(); assertFalse(metadata.getVerifiedSignatures().isEmpty()); } 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 3436ba69..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 @@ -11,6 +11,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Iterator; @@ -22,6 +23,7 @@ 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; @@ -31,11 +33,13 @@ 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,11 +51,11 @@ public class EncryptionOptionsTest { @BeforeAll public static void generateKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { secretKeys = PGPainless.buildKeyRing() - .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER) + .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(); @@ -120,21 +124,21 @@ public class EncryptionOptionsTest { EncryptionOptions options = new EncryptionOptions(); assertThrows(IllegalArgumentException.class, () -> options.addRecipients(Collections.emptyList())); assertThrows(IllegalArgumentException.class, () -> options.addRecipients(Collections.emptyList(), - encryptionCapableKeys -> encryptionCapableKeys)); + 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.buildKeyRing() - .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA)) + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA)) .addUserId("test@pgpainless.org") .build(); PGPPublicKeyRing publicKeys = KeyRingUtils.publicKeyRingFrom(secretKeys); @@ -148,8 +152,9 @@ public class EncryptionOptionsTest { 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(); } })); @@ -157,7 +162,7 @@ public class EncryptionOptionsTest { 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(); } })); 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 4ac43630..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 @@ -26,7 +26,7 @@ public class EncryptionStreamClosedTest { 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" + + "lQcYBGTKPr0BEAC7+3S4hkOwLV4WK3VV+3OALMkVaobplxtyOi/3bZaXC83zh51A\n" + + "wa5rcZDhMyPKxVZwvuJGQqeeQQQu/n+rCHp3LrDR5J7YTZueQ022xzvvaGZEkT88\n" + + "rROTdV6FXO0P2CaFlk9dPH6PwHzAWjszdZ6MjPIHkzcSFj6VSHwd4ckOqLGu+LnZ\n" + + "bqRNwdV+L6HMyCJsMk4O2xkByBb5xDM9kMOy3toVMPj+9i5NS7/lLq/9VK9AstSt\n" + + "77f5j6cfxsPiSYZAOI9aRg1HLSl3iqQ3o0ZfPNpBv7+nQBcMDkRlzZe+ymcH1ZK3\n" + + "o/CpAZjfWlSzfMsENlinP/iCAsntGb9YzPtO2fZm/Xjz/8mSkTcM48VFqxKEVUCe\n" + + "qeoDjNaURx2KjXf1fJKKsYKYpalDgOqmmw2uma02jlMNRbO6JHMdESvVAjReXB7j\n" + + "oXVwpI660sgdG8Nya8/KhDOpR8Ikr6Gd7J2NH+0N2x25SBFafMP8NFySr3gJdSYE\n" + + "66Kx8vx/3RWFOXtcrk4Xn2xew6jrHjjdbLO3+92fcyKeHF8jtsYDQQURW0UTDgo6\n" + + "GSEc3Zz4px73gnU/YmqtPeOiywwuDagnH2ayOwIK5P5a/JgaeST9k4ZTjlBhKm6d\n" + + "0EXoOy2Ok4zOXB1SAbH6UzevV/20V3/PCN7+zNdn5u4o40uzMIbRxF+9kwARAQAB\n" + + "AA/9E1Cd0xyUlY8WdO+yhuBRzfs2uW5wffg1QpI2UyrdnMKClFcRsibUcvIVzACI\n" + + "WRwtVWNU9kQrWzLcqQQkU7YIzfiBrudZ55QTJ8/24GTDRJufr8Q+0PoK92Keu37r\n" + + "VW6aXVfZlWpoVKvssDT1PUSEwyCeTmdIHXckp2EYbleV5DLdeELSkcPxD5OZifV/\n" + + "Sf53BFKnkOuJO792ljjxzOMe1eEDsRwO5t+eV/nZte+LTLEoBV1P8K6fYtM+/aBb\n" + + "iIORJEXLe5/pWwVfFosryWhg4YXr+nJ8e5pgY9rkGJO8gkbjZDuDxOkMNXQBDv4F\n" + + "6EqrxNCIC733x6AIEHEjUeP/SZM2pwXL7B/j4xwIvf20dvu3tAvLXWbhRXQ1vLYC\n" + + "xy4yzvwo7+GPKakkkPEWthwRbfengCnEjeEjB54UPditokeVaajZ5ZgG8eBtZ0yS\n" + + "5SQrFGmz3bXXLKZLWgr5aFoOdkKT35iJUejsiI29IlTdMy/hYgMyt0+vmElmOTqC\n" + + "PhYYRogYZe8ULxwlApnDOeFCuosppDGs3X45mKO1hnKWnYVvv0F8QFIX6PLB5NE5\n" + + "tbPIxad7XwYEgZVQlx7r6ob9u5r1mA7/hNykjHRnHRmhYJs4ocuvLXzGD31f0q02\n" + + "Wz4dQGPKRIhBmPPSwlwr68bzmnDOV8kOMQix8l78FtgYnHEIANOnKJpvk+pT+r/7\n" + + "0lQYP26keSxbATibFawFVo3Xpp7ujok0I/gYVgiuVXeSFq4KOtRFunBBw0ISBqSB\n" + + "DCoZh56BIOLBFGP5Kw5sBYyTTf1MmgBc2rxlBBJwO3w3I5l0jcbbIboFYKsVpda6\n" + + "3wOuYCAFL1Z8WAjoPDiSl0L1tsvpHzOtCufUrxvtWByopdu+3377AWuua/VVyQp0\n" + + "xwyz2pxd2GcA4APeiNdxICt5DIK/d17U5V7AKx0GcTluLGCqGBOX1lScGi7DAuXv\n" + + "LAAMNUo+pWErEtuNNc0Weq0/RIL97uCWeaitZhFmQNOYStsN7dlz2aS2//rgPT1w\n" + + "Mlu0TXEIAONeoBEs7JhqK0AJmLzV3DBNOzNcf9MMupzuRsMoaEHR1ibKKKllRYkE\n" + + "3JmOupbaVEVWw6lafdY0bkKpRc5pcfW47vWAnb0WhEZloN3HaDLDY7WkI+gWh4zF\n" + + "ihhmgV2dlV/EwqS9CalkPzKLqWJZM8e5SfTRAOgrGVDB/tOp/pUr0u29qAYS99Fl\n" + + "Dqo7dYjPccOFI5VZmlwkkrrvV4JIBbIMtN/kAfC0Te72Ka7HqSj3P7amgg0k8KM5\n" + + "rxXeI4P1fAzFIopa69LXY271XFqnuSfvGXRPQHhIo2WU2t9wh0oYu+3XhbcWUfc0\n" + + "QGqunPorNwP49x0ESIk2m7jXbqueiUMH/3knMUz84JKoWwnAkYbGipNTVdE0mHJ0\n" + + "V0CQrNvP5mCU1stXs809UJU93oOnsDAklb/qSFn9jDy2LwHBG0lkV44Si0VPHKa5\n" + + "83q2yZpvrnipFefccKDgnnhEkz9d5MyLVjnFSO5smpEfeDk9yE3RZIORRxRbnQ4I\n" + + "Jtd3gy2/GJQADatHsuWa27YswaP9glqEMEtxqqCirn0BaM5TFS+kf+bDpsPaqFcY\n" + + "zyD2bd1TxlWs+HelhezLwFvsYGryvubZdctM54wfI7v9pMZMXw0qHqQBlZvFZ2p0\n" + + "FEJV+FLHcBbl+rH+EC0MJ7HufdcT5kVzYt9bRKrnH0WN+0yB3Y2yqdOFXLQXVGVz\n" + + "dCA8dGVzdEBleGFtcGxlLm9yZz6JAlAEEwEKAEQFAmTKPr0JECUDYDMdgeJkFiEE\n" + + "qZGnMpAhfS0SUJPDJQNgMx2B4mQCngEFFgIDAQAECwkIBwUVCgkICwWJCWYBfQKZ\n" + + "AQAAA4sQAKTIO/N+QoqiuaIy28rpesCviltdCYxNAL/jXwrbwfr+D/RIuNKN1jGz\n" + + "7cQ43YDE+uMJnvnPrXfyP5vxxfzO+ol/EFaXL1X0sLVAKEJzEIPqvZ1zT7i15tGE\n" + + "wJwjimDXtdRu97PaEZ6YiW86zJcn31LFdAppBMJcMtoY9T7Em4zZoTtbgt/4IY0m\n" + + "0+hBe/uiNu3j/UNpoD8jKzy+8glJOWPlygb9QZ8o9Ckd9X9PXs7UTP0Fh9ka0HHc\n" + + "lC3GrDj5gLhrg72WkhNVa3eFVfKIGZuVrdI05LIz+m86WdHD0AZ0wkSn1voOvpYd\n" + + "bjP+gWCrq27loIPjI7Kv/mdJ9vaXXjtNP8QvZvTnxSI5CxY3GnSiB0OzRQSSpAgH\n" + + "fFvGXE5bDvQ8g37Yd1QHwSPV4ltxgGzaMMfI6luujkpcNmZC6R214Lr3pZ31qi8G\n" + + "Lfb7k0QzftuQh5z0vtcFLaUuOz7VGwsHMKnObpoQWq4ynyLbM4oK0fvHgAtF/6Ym\n" + + "EQNvH6nh5yYEnmWWIie4/2VtkJBRikirjxS2lNJGg/sLYWs08rYCVAipLtttYikD\n" + + "UT2ejtCUJiI0BesgeGCHxPbdSAC5CmquTqISIDNbOVg3/aZ6vqFi5zAmIz7OGzhc\n" + + "fm7r2cCw3ZdltPmBCM3qZR8cTWl58NVjSz7VWoLsNcZIk/CUnJgXnQcYBGTKPr4B\n" + + "EACVef2KJ6VQ/WU32tz1NdFzbN2cfbc4DD5xmqWT6vIEJXq361eDVXuTumVXcORP\n" + + "oHewz3OMy7ZUZ4i4jcUmGxUVfV9nxYD+Qv547NPcTdlnyx6NfCcy2sLp1EO8k8a0\n" + + "4v2rzk9UP1k6fD+9aZr9wUoLgox7k49PYBXmqBbeQiuMuav2uGumI5JorOsX7+Mj\n" + + "PQ7KDoe6s4FlTSOgc64TAkHBvrNLFe+R2hbXG8SNA1QtNSZ2lawctCjIFKT2vVnz\n" + + "oT3imuEA3DfEJoUva3RWrhBwpql6rP9U8P7/eTMf1rymMcoFvcgMVoE4ZnQ9wUPv\n" + + "ixdueclYogPbjlpR/uoQmsRKVeLM0+Q7XM0UvGh0FvEg5L5waHW4M7c52y3D5VtY\n" + + "AsEhWu/ZXN8qk+6L+7crGn+YiKjZrG8bhXR1EEBbNIpM1bb4CBkl1OVtE6jammk0\n" + + "kN2PDV1tBtMWHwzlgnWuIBXr5b+S3AzrtODSxMYwMgNt7KgEa6PccOgb+doBJjDN\n" + + "pMQIoTiQX5ynUdxguqKlRosHWXCdy0yUPgzDzOULipFS/X4Gagxw7q67CbJoErVV\n" + + "VinwTdl7QXhK8tUWuQGVA9ObjVcT94OBhgBjzWQLe255o1VcR4qhn/e81kMRkiua\n" + + "KkA0C3g8of2sH1MFRE54N61YnQ8P4PZNPYLAjEF0nRT1hQARAQABAA/7BXPGLkBk\n" + + "9M/RXdizX5Rfd/TcHoWtZbN4oZ8w8/TJcCJH2CaS8hzvnYNah/Z7tXXWd9IRVmzl\n" + + "0S1XnNe6/blWKwsALGJVYrDh5FpLHgmO6QzNJ/8D1QSKwIm4EMxZHqb69sXXOez3\n" + + "nb0DfC66cxAWWdYgtq86tnv8QIYYE3JZcVAieCTg9FXu1Led+akL4XCsNe2SwNok\n" + + "WaQXLRabHmFiMaV5l78Mlobcd2sxX61j6CQ8q22pMgDWTfoGzGM6wTq77aSVmXju\n" + + "5c475G9odnLx8ZH6s5lU1O3Xd00d8sbb6bn+MvhpsB2FqB+AlPIUPswVhjeWAxAh\n" + + "0OPf4obIVeO3TiqOy8x2ptPeaLdW/v74U/zainkTdgF59P96d1O68qInqTgOUsCD\n" + + "QMSsyj+seB32Nz52qW+BqhjUPs4H3ygeqAz8q9Kjo44NRL0YoBZ1Gvfv3UIuKlTA\n" + + "465J0b8g+XqAC8A4DwxmXIrzQ5o9f3JUHF3arl/Dt+Rcx9VriWU5d9LtldxFGo8u\n" + + "7RdiEIpFw0ieKBhc8z0hqD227f07Bo51q5gcgG/xfNmKzvgYJGNvsLcdT3J/18nj\n" + + "abmj/Vuzk2Z16FSv5kh6fWjn7J8GlW8v1d5TMKzVabBgAyENmtYXeMZ6EG5bmbpw\n" + + "Q79GAAs5w1PPIaAZZnSKhTnVq/4q986ka/EIAL+9js8TZTwWK/h83CWKaOqtTX1V\n" + + "2U0hKDF839talkdxmZP0sAZHMbkf083jh6bSzjE0Qm25QObL8TECehTzFfstRk9y\n" + + "sZAjkoLTiH5ZThBu8FjzKzunV5sv7586KeQx6KGFVAv/VuQsj+99Q4UMzrbP+gvH\n" + + "VYukJSOYKClFPfpsAXJTajHIpVBcKtBQwdSgN5K8fpyDrAg598onKGZ+qlZ4EzJF\n" + + "hCA66TVidiiMcOu0rgNmYpinL/6WwrjxB34ddcLj3LnJ9DITAlLpGWnveLhO9d6M\n" + + "VFMdDPezpbGJ93U10utH2b9f4iUmH5NHi2I6VhAtuSV2bOJZAAlfBOwcFP0IAMeS\n" + + "YgboyyYY41ShZbibyVwNPwRnqopHMxT/5BJTmx1SD33HuZtYYj1TF3CgvUSgZj8M\n" + + "gnOMgYVm2rFjV0x7TIYuJvPEr7yp/rkq3czmyRnNv7zGun5PTih2GKTh0RL6vhx2\n" + + "uQ+e9oCDlS9xwnJupkg0b2uT9NC9OSh2jlFgLXl3e1ahKje8T7hyx2pQb8zKO7ir\n" + + "fXeeFaFgq4uMYXSwE3+oPZkhYppV/mOx094M0XtScqmfPYcCFko21PhqLT52nJkA\n" + + "yu6b9BenLhSPx2rVlJQrFBvFiqHc1A8Zy1Phou1HwI1B4uhmZzxUs3EdWtyTQFEe\n" + + "9q2v/ClgfmZnEhUTzSkIAJdINY3Sl72tuI5dFvRcsv0Ypmg9iSJel2pV7S0beCV6\n" + + "58ihgw5WqULoUZEacnWUhrRMfLUus8CshwfSjP2887GY2/V+1wTxNUOyBPA+ZJPM\n" + + "EcAQX4mitA474vW7ADYTgGDMasvB7JEYrklRl2YrQbEch/9d2VrQonqVTdw48n09\n" + + "VBOHCg6XVOEEWwwb3DSbVA8RQd3Tuv0LcBpkre23GvLrPhzcwxEJMlvkiuUk6hBp\n" + + "tL50ofbgSsZlHOfUpD8FZQ//Jdw8pSiuBVRMiLjowJbR1qApuRG34xdk4pmuLjVG\n" + + "kqlLOLWdycWufSgPIM1wYN8grEdWNsxu0FaASJNTQYqBf4kCMAQYAQoAGgUCZMo+\n" + + "vgKeAQUWAgMBAAQLCQgHBRUKCQgLAAoJECUDYDMdgeJk1D4P/2rDvSBMKTUo2RaC\n" + + "iavhbE84WytNqHaBt5AwY+cqj9WryffA5yqqnOYTYnzxbUK8MFoIgCpRaMIjIue9\n" + + "IyC6SRxRuvRGGV4nj1zgnCKGxQeIv1QMAsj7b0igVys2D6wwqvfDjwTqkfBpcFb6\n" + + "OTXkrYD14hYp2Q0fkhHwScReQu3PNnpiYnMqI8prnwIH3hFJfgWQVccYDSEtJPiH\n" + + "vW4xmWJ2R1NXFuVMMTfVHI7IiTS4yF9NHp0W8OsDbBPK/XhbYFU/VjMOsERhuT+5\n" + + "9vFydJsBY1s2CieE73qrIhzkG4mdxbH68mA30KbSTW4qb606y1qicuNSjH4f3Wew\n" + + "vws6REpWo5sMdX3z7mMNrAuWI+jLXmaZqUbxwtQ5YizSXCx2u1xkP6t/c5T1axiY\n" + + "Oqewjlno7hx5hwHwaDvqdxJulFXWV//7O0R0FvGCqHS+TrpRTEyY+r4yYN9fSp/r\n" + + "/h5KvL/QspRset6CqQvFlRa5aARYjU0qTb4KwtGHGlaKmDWA9Ipf8U+//dxyDy1+\n" + + "pWXL5XH+DyfUzpDH5XgPaa05QwX8Wgfk7uYR3IOtVk4cL4B8+O16kIVfQo7geUpQ\n" + + "heetrKk8fTobRwIu+vN4xWEIqrsX65EmvMWV/DGYmFKHskjKmc7+z8o33/spthHY\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 2b0a7106..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; @@ -123,6 +124,7 @@ public class FileInformationTest { assertEquals("", decResult.getFilename()); JUtils.assertDateEquals(PGPLiteralData.NOW, decResult.getModificationDate()); + assertNotNull(decResult.getLiteralDataEncoding()); assertEquals(PGPLiteralData.BINARY, decResult.getLiteralDataEncoding().getCode()); assertFalse(decResult.isForYourEyesOnly()); } @@ -164,6 +166,7 @@ public class FileInformationTest { assertEquals(PGPLiteralData.CONSOLE, decResult.getFilename()); JUtils.assertDateEquals(PGPLiteralData.NOW, decResult.getModificationDate()); + 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 index 242b430b..59ad4596 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/HideArmorHeadersTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/HideArmorHeadersTest.java @@ -23,7 +23,7 @@ public class HideArmorHeadersTest { .onOutputStream(out) .withOptions(ProducerOptions.encrypt( EncryptionOptions.get() - .addPassphrase(Passphrase.fromPassword("sw0rdf1sh"))) + .addMessagePassphrase(Passphrase.fromPassword("sw0rdf1sh"))) .setHideArmorHeaders(true)); encryptionStream.write("Hello, World!\n".getBytes()); 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/SigningTest.java b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/SigningTest.java index 156e6b57..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 @@ -34,13 +34,13 @@ 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.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; @@ -106,12 +106,11 @@ 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)); } @TestTemplate @@ -188,7 +187,7 @@ public class SigningTest { throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() .setPrimaryKey(KeySpec.getBuilder( - KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA) + KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA) .overridePreferredHashAlgorithms()) .addUserId("Alice") .build(); @@ -208,7 +207,7 @@ public class SigningTest { SubkeyIdentifier signingKey = sigs.keySet().iterator().next(); PGPSignature signature = sigs.get(signingKey).iterator().next(); - assertEquals(PGPainless.getPolicy().getSignatureHashAlgorithmPolicy().defaultHashAlgorithm().getAlgorithmId(), + assertEquals(PGPainless.getPolicy().getDataSignatureHashAlgorithmPolicy().defaultHashAlgorithm().getAlgorithmId(), signature.getHashAlgorithm()); } @@ -218,7 +217,7 @@ public class SigningTest { throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() .setPrimaryKey( - KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA) + KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA) .overridePreferredHashAlgorithms(HashAlgorithm.MD5)) .addUserId("Alice") .build(); @@ -238,7 +237,7 @@ public class SigningTest { SubkeyIdentifier signingKey = sigs.keySet().iterator().next(); PGPSignature signature = sigs.get(signingKey).iterator().next(); - assertEquals(PGPainless.getPolicy().getSignatureHashAlgorithmPolicy().defaultHashAlgorithm().getAlgorithmId(), + assertEquals(PGPainless.getPolicy().getDataSignatureHashAlgorithmPolicy().defaultHashAlgorithm().getAlgorithmId(), signature.getHashAlgorithm()); } @@ -247,7 +246,7 @@ public class SigningTest { public void signingWithNonCapableKeyThrowsKeyCannotSignException() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() - .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER)) + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER)) .addUserId("Alice") .build(); @@ -263,7 +262,7 @@ public class SigningTest { public void signWithInvalidUserIdThrowsKeyValidationError() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() - .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA)) .addUserId("Alice") .build(); 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 ba832516..d97891d8 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/example/Encrypt.java +++ b/pgpainless-core/src/test/java/org/pgpainless/example/Encrypt.java @@ -195,7 +195,7 @@ public class Encrypt { .onOutputStream(ciphertext) .withOptions(ProducerOptions .encrypt(EncryptionOptions.encryptCommunications() - .addPassphrase(Passphrase.fromPassword("p4ssphr4s3")) + .addMessagePassphrase(Passphrase.fromPassword("p4ssphr4s3")) ).setAsciiArmor(true) ); @@ -207,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); 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 1e743fa1..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,7 +27,7 @@ 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.UserId; @@ -37,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.KeyRingTemplates#modernKeyRing(String, String)} or - * {@link org.pgpainless.key.generation.KeyRingTemplates#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. @@ -74,9 +74,9 @@ public class GenerateKeys { 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.ANY).get(0).getAlgorithm()); @@ -180,7 +180,7 @@ public class GenerateKeys { Passphrase passphrase = Passphrase.fromPassword("1nters3x"); PGPSecretKeyRing secretKey = PGPainless.buildKeyRing() - .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), + .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) 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 858c99a9..3b29e35d 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/example/ManagePolicy.java +++ b/pgpainless-core/src/test/java/org/pgpainless/example/ManagePolicy.java @@ -43,7 +43,10 @@ public class ManagePolicy { @AfterEach public void resetPolicy() { // Policy for hash algorithms in non-revocation signatures - PGPainless.getPolicy().setSignatureHashAlgorithmPolicy( + 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( @@ -83,7 +86,7 @@ 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)); @@ -95,9 +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)); 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 d61aeedd..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,6 +4,7 @@ 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; @@ -112,6 +113,7 @@ public class OpenPgpV4FingerprintTest { OpenPgpFingerprint fingerprint = OpenPgpFingerprint.parseFromBinary(binary); assertTrue(fingerprint instanceof OpenPgpV4Fingerprint); assertEquals(hex, fingerprint.toString()); + assertArrayEquals(binary, fingerprint.getBytes()); } @Test @@ -122,6 +124,7 @@ public class OpenPgpV4FingerprintTest { OpenPgpFingerprint fingerprint = OpenPgpFingerprint.parseFromBinary(binary); assertTrue(fingerprint instanceof OpenPgpV4Fingerprint); assertEquals(hex, fingerprint.toString()); + assertArrayEquals(binary, fingerprint.getBytes()); } @Test diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/OpenPgpV5FingerprintTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/OpenPgpV5FingerprintTest.java index a250bef4..57c98928 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/OpenPgpV5FingerprintTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/OpenPgpV5FingerprintTest.java @@ -5,8 +5,6 @@ package org.pgpainless.key; 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 static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -29,77 +27,12 @@ public class OpenPgpV5FingerprintTest { 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 testParse() { - String prettyPrint = "76543210 ABCDEFAB 01AB23CD 1C0FFEE1 1EEFF0C1 DC32BA10 BAFEDCBA 01234567"; - OpenPgpFingerprint parsed = OpenPgpFingerprint.parse(prettyPrint); - - assertTrue(parsed instanceof OpenPgpV5Fingerprint); - OpenPgpV5Fingerprint v5fp = (OpenPgpV5Fingerprint) parsed; - assertEquals(prettyPrint, v5fp.prettyPrint()); - assertEquals(5, v5fp.getVersion()); - } - - @Test - public void testParseFromBinary() { - String hex = "76543210ABCDEFAB01AB23CD1C0FFEE11EEFF0C1DC32BA10BAFEDCBA01234567"; - byte[] binary = Hex.decode(hex); - - OpenPgpFingerprint fingerprint = OpenPgpFingerprint.parseFromBinary(binary); - assertTrue(fingerprint instanceof OpenPgpV5Fingerprint); - assertEquals(hex, fingerprint.toString()); - - OpenPgpV5Fingerprint constructed = new OpenPgpV5Fingerprint(binary); - assertEquals(fingerprint, constructed); - } - - @Test - public void testParseFromBinary_leadingZeros() { - String hex = "000000000000000001AB23CD1C0FFEE11EEFF0C1DC32BA10BAFEDCBA01234567"; - byte[] binary = Hex.decode(hex); - - OpenPgpFingerprint fingerprint = OpenPgpFingerprint.parseFromBinary(binary); - assertTrue(fingerprint instanceof OpenPgpV5Fingerprint); - 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 OpenPgpV5Fingerprint); - 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 parsed2 = new OpenPgpV5Fingerprint(prettyPrint); - assertEquals(parsed.hashCode(), parsed2.hashCode()); - assertEquals(0, parsed.compareTo(parsed2)); - } - @Test public void constructFromMockedPublicKey() { String hex = "76543210ABCDEFAB01AB23CD1C0FFEE11EEFF0C1DC32BA10BAFEDCBA01234567"; 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" + + "lQVYBGSIp4gBDADhFvBqIHyyYdyhiXH0UZNDDIcKY3g3KLRFN0DZU1LNUkm1E9Cc\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+zMAEQEAAQAL+QGFZzPTeda2nKqVaSkUF5Yscp1UKl7sv2D4NkYE\n" + + "KkALpuQYB6+uV9LbKtAvlw/w2/n4OPD96/QkdkVg8GdajWCgaBhVVzvpTZ6xE9gm\n" + + "UJrAUoGJCKzzhEOUl4ZdEkFUn8BvEUFWkJ2ClDa5iRQ9FRiVIwWUv1YgkujGE6l+\n" + + "XDXWQ2vr6VP50QGRI39hrQzaZDAcHFwn6oYm+IHBkK/1Dec7UXskE1adO51NX6dJ\n" + + "kIPwV1y5KPKWFKBLISrywi4/N/ZwFbwJaMbnoWJIS8gqwZzvGTZpifj1KhzEg9+h\n" + + "yw2/mlaiFZZ0Xw3O8rWG438Qa3z25Dp3Mofoyc9NBjG7QM7r0WYpOA+UpZD27yHV\n" + + "xIJMwck/0frgdJLQqeQ8Aqpetn2rEafV9F0wKYictzF+8k0ZpMp+wkWoyGPLx45v\n" + + "lO/XbcWVleAeNR2evCImQxIRLKMCVT49v3Bv051dMNGKbHgXih7Y8JMy81LQf9xa\n" + + "IQmgatJ9Va4M542Rupwf//WCrQYA6FeyJvLSdZYzIdSxjo3vjkNrlKVoxXtWiplG\n" + + "/Uk/iGHimIQVAmHZ4aGmsrHNnuhzZq+/8TZvvISB08u6NVotSMh4EMj/7TZF1WST\n" + + "thO4nKIDNnqGvHyNJuSw6bInjRZzQ8pEMILNvxwdA9CW4m3GbXTWdMLVBi1RO+5w\n" + + "PBw53PuYrP35xTR1NrsEtsG5AWizSv42s/8GzRI+N4BhVVym6Um2TV85Ow2ixyXb\n" + + "wKwTeVvx+5gWGfrC4B1AkikDT4oNBgD4Ai+JnwEOxoNNkhCnjgB7O/mgTdpMx4Qj\n" + + "+liw4doQKoZdoeciG6QQdt3oquKBDwLMJ/l6wPdnyxwufLzUlydmWc4uwNbybIDg\n" + + "X8VFdcB6dg1hUp5xC4iI1xE6jMWenyX/o5TDD9C4US9pitFZc6g7jiBgz8B5qap5\n" + + "hP7JtVeGAqyZGxuHxo7n1u2d+UaV5MYmt1aF2hN83SzDQtqVjb3UXzPSVuOgJRYu\n" + + "oAi6rV0bS7dDVjGdTZtU4dVxj0UUij8GAM3Ulwad/nthRW0hagJmwl3NN/+JpY0x\n" + + "iUg3mJ/5b0UAVzhyjljm9jGwWAkBrAFnl/vkavVUw7GGwuFVzWQugJDFLAgFda9/\n" + + "l6aVTGqWWApzz02Swkzz/u5C/DQgIO54lW4UnfYfyPxbk0fS/uJq5kNeisz9SFgO\n" + + "DGhyTtL499QywwX5Xy847fDVdkV0gQ6FPbEZWpE7PdGjKmu7vszx7QYfGZ1YYLmM\n" + + "gym1RMcTS5ZxCU/GebbMyRPPAFeaU4n4fOJHtBxBbGljZSA8YWxpY2VAcGdwYWlu\n" + + "bGVzcy5vcmc+iQHTBBMBCgBHBQJkiKeICRB/0mCxlcKnjxYhBIE5015AyxpuDg+3\n" + + "aX/SYLGVwqePAp4BApsBBRYCAwEABAsJCAcFFQoJCAsFiQlmAX4CmQEAAGU7C/9e\n" + + "uw53qZdEmFETyjITdUNBESyUMKo+t5rycua3iFIRwyrb+I3zSrjwefRzLGopovxO\n" + + "9eyZnuSnSCK3UgcGhfsLB02y3w9FfEV9H4USmH/ensGODN9OJ9LZjgMfYWQgFmeV\n" + + "AZq93Nh2IwzLyfW8JjnpnwZis2XMTU+oW82choWUnKCkEPg+JRlkEux0hpQyPuy7\n" + + "cW9rrzp+Sk/Z/9B9a7lBAREfD3gAcDke6JhCTKMstYZHpRLE4Q7w91Cu4dR/7ozZ\n" + + "uPgYALTNvP94C+SfmRXK770/P387QxAVjz3pZSVjzsmeGXZwpHqDYhEftLQcYLw4\n" + + "9LIWw2LKw2j+s0grZFcePrnw78ap7aQd9+btVWxrf+y6F+b5jsseTNVt5twLwFgA\n" + + "bunas8OeHl4/42iJBMBHui7ET6UZ68CCmpuaGbpSJPF2E4qy6p/ALWaoSFYWGpjM\n" + + "P6Wcif6M38LoOWQc3mOFBHZslIh379waHX7g+Cahn73owf6A+YCxbAn80BznsrSd\n" + + "BVgEZIinigEMAM8twCelWoNEs61GDrxydtp8cOeskMJ22tDopJreNNUsgTEJnO7Q\n" + + "04Xuqc2wmVPRgUAf9F4h5q2doufGHKrYd9M+XcJZ2QMVpg76nP4KT3Aq1WK/obFd\n" + + "rjg3BgF93ZPZYD5mmR3c0OZLs6wwzArshCdBj2Y700rhD8wS7iRwRI+RqPu1OM3Q\n" + + "dMQ0yH7C3/4L1REfyXsHpf/kE7+xMiagBoMakV7fWJ3GyZsGLlgwoTpzhw3hefle\n" + + "0UsxBRd3Zz9Lyop+VLlD4/mPx0P03mF2/1xl7llA7NaCGADlNf5RkRnF9xHd6QcG\n" + + "xCmg2VAHCJ0KBSrZXbdaa0fsySP9Eft8Fz3u44LZTjGrWJOZlj5hiNANbMHwaShc\n" + + "bousamNYGXneic0E3jKriQG07P54UUN3WWss4nc87YhTscvpOj/tkdQv1ViasWfS\n" + + "ceSJPbidQ/pDE2F/LEHL+BKNkO0ffcvoOvnHb3T7dGGF5RkV7poJNWPAQZS8VE01\n" + + "14QlamnAo1UxmQARAQABAAv+KCh61OR7CzNDraxE+neofJi+7NU4+Qy/RNu3XlKA\n" + + "hKqb/ySZH/xn58/XZ6iZy9Kx/jtDa4SgOVpzI4B1+ZK8hTyTQXMXer7VQZWH3UEj\n" + + "3T7cPkgdYCsjtD9MIXlje/9YZbgO6WguMNmPIesSffKS+iX91Qf3II5H+NtZgEQn\n" + + "sfl+uw2JTXGg1JBTpaiB8PUbr3ZNIFUe8t8jidZfvXdq07pQX8akn26TEQLNdGg2\n" + + "tEUE8maYdUMRCAVgMoiBQZZfuo3EFVMV5ev69hMXDQPKFtOfH28f+fL2gqTEMmDL\n" + + "Z+92CSq65fi44LclVR2ihRp3TjAVG/lXZeAwNnN4n1jGEEb8eA3KNDDiUSVXoR2r\n" + + "QgNcfQTa9XGSvxgASgWG+tqxQJTPbigN++9w7RY6yzYcPs7GG7ZFRITtsMTY5gp/\n" + + "ZMpQSGyi7Cp582xIzJivPegkTdITGCJNIwIjT5Zd+QLtiAHoOf0VhYDI4MFQDcCh\n" + + "v24IQDYVXfLbV+pJAhlEGbFlBgDfHOUvO70AvyUyGmW88YVfyBXvxkzVZOtmCxCF\n" + + "8X3FNNjG8Uz4p5Sor0dFfIsVzgo+N81fimqB8dgmIE2b9eD52/KcdFEv4SKlSL5v\n" + + "NtpDUfq0hqPIZQFtFJFaKy+1N0Nlsvs6WFtSAONY8Afn8CmGNgInBTSnAjbp3v4b\n" + + "prS73fSoKk65cVSpiOWV7l54rZXQns8b5ltncCOse0c73sDX/Hzzddnnj9Sa48Br\n" + + "c+BoWGLaOi+BqF6D0rSROeBiej0GAO23lYjZeuGKnwkkOzTI9ux+2iOTX7Y/0mqC\n" + + "3q/zVoIt5iFvcn4x02I1+0qe+PJEw21BbR2ZNiCWoFltADWE0sH8FVRcSZIzHQmD\n" + + "X9vBg7J66vGM+kDqikBZLSqSUIhEv4dV6rWM7VmqGGXFtpbfoafpmkJVzMjo7O+k\n" + + "XI8YppEsluGuzirlxkpT1f6ClbYSsVAUPiEgwjYDq7M0Lhx3J72TlYpTxYUf25J4\n" + + "b2x+GKjtPVBIpceIY22BevppfHs2jQX/TmSZ3PMbUBd6cax6hv8TtJwje0KhgdIu\n" + + "6LKek2qRx6Xkd8vDwkamqq+ww/SRsmOA2e5fi6OOa+89HgJKn2a1E8jji/DkDyJ6\n" + + "SWvnY8GAMoCfqzNu1pl6rlJmnSbTMJ1oKTBySJxq5xcX8iJfwSBE5FAvYS5OW9K3\n" + + "b0vQoc364NVojrLV4uZgPRAA+aaTWbqQqTbgnxCEX25oxJIdxbEeDKOkmRLomn+Y\n" + + "b+1owl+e8kRAl3DjKiYVI0ROE5r3F6yg5jOJA1IEGAEKAbwFAmSIp4oCngECmw4F\n" + + "FgIDAQAECwkIBwUVCgkIC8DdIAQZAQoABgUCZIinigAKCRBF2Kvk0Q8YjP+SDADN\n" + + "x82Dc9qy2fjydKzOEWEKlQdG6e5sIVi6sCCDJCA5bejd0dcgi7yFHYn2KqfDVdVJ\n" + + "uWMItgVEmP5zdJLSwjuZCZBffQY5TFpE7++Oe6PY3aOtsC87mMPgxcpJRfsnrTyH\n" + + "xvqBQGr/MgZLBMndl5fas/N6sZw/CLOjOS2UKZV2uqn1dLZ2BUM8T8FwAfns7SQT\n" + + "aeGR8neQdEB64BIZaUaWza+yXq25ZJWEUi9bwmSks3B94WteDZRBu9ZKpeEYHlTt\n" + + "7NQWVJ94WBbXWul8dG1o580vz0qcHbO9WlZj9KdPj7TAwFYj/08AjoSkLliXgr07\n" + + "WP0frjwFE060zaPp9K/V1G017WTfWpU+cLX7qjAIQ2+Indah3Ffvfd+PP4H7BoKZ\n" + + "IrbouSlckUDBQVZcuNtw3pY9W6FzZNCVSc66eRtNtpDqj4eNnWM9XLoHmwamKfwW\n" + + "CyCRELuOsSH4w2PxtogDigZwGdft9Gd85/6ygf7OB2pTGTzKpI5avEJ97K5zDb4A\n" + + "CgkQf9JgsZXCp48vPAv/dU4YyeXYMAHx95PQjdi+Mo78iVsk7yKdkk27YDCNAKMm\n" + + "5dntuFLTCxw6gpgWp7zJdKkGgXZTbI/2PUCEfHHeUJwC6bauUWDG0GGdPud5PD7P\n" + + "K2UWhR7BD9UtaDKyDVFfOHV7mGimaeRkkHVNrUCk6Kcd4/f8hhXLoEROkZS7PS7x\n" + + "Qn4EGrUPSJbIiq/Ug4mG15mWuv3aw7NDk3CIXIJuLlSA9dHcO4UyxBPR61ZYj0pR\n" + + "0MF6x5REJw+KSoExLK6oXd+lDRbqhbi3A2+NjuafVcv2gpPz1Jf4Y3MRXgZiOrz5\n" + + "r6y+BKwOye5ycJoOc9I89yfMwCmP+VgwaYBbavnExyK0GVloigDkkE52Bk2z9h+b\n" + + "rJfQtJ8Yel6uhHkudUVEHkhh2WZR14eMS1STMoNW1oZL8nSbOwGrGUjW7s1hpask\n" + + "2LLiCDB0UFjn8HLQt7xSfP4t/cbemenHX16CVGQnyKvWbAhHkeYe91MaWduqPT5m\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/UserIdTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java index 93cb6922..1290ad9c 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java @@ -197,15 +197,14 @@ public class UserIdTest { } @Test - public void asStringTest() { - UserId id = UserId.newBuilder() + public void toStringTest() { + UserId id = UserId.builder() .withName("Alice") .withComment("Work Email") .withEmail("alice@pgpainless.org") .build(); - // noinspection deprecation - assertEquals(id.toString(), id.asString()); + assertEquals(id.toString(), id.toString()); } @Test diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/V5OpenPgpKeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/V5OpenPgpKeyTest.java deleted file mode 100644 index d1b941fd..00000000 --- a/pgpainless-core/src/test/java/org/pgpainless/key/V5OpenPgpKeyTest.java +++ /dev/null @@ -1,61 +0,0 @@ -// 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.Disabled; -import org.junit.jupiter.api.Test; -import org.pgpainless.PGPainless; - -import java.io.IOException; - -public class V5OpenPgpKeyTest { - - // Both key and cert are provided by Daniel on - // https://mailarchive.ietf.org/arch/msg/openpgp/Z2Mkq9TfvgY5jUJzlNRwgDsDSUk/ - private static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + - "\n" + - "xVwFY4d/4xYAAAAtCSsGAQQB2kcPAQEHQPlNp7tI1gph5WdwamWH0DMZmbud\n" + - "iRoIJC6thFQ9+JWjAAD9GXKBexK+cH6NX1hs5hNhIB00TrJmosgv3mg1ditl\n" + - "sLcOpMKkBR8WCgAAAB8FAmOHf+MDCwkHBRUKDggMAhYAAhsDAh4JBScJAgcC\n" + - "AAAAIyIhBRe8+DZtlDb3rzfq2hVsZJRlblqXac8tXLNF+Lg0NvZSecUms7MC\n" + - "rI0Ofp1iKV6QwGFEAQDnd37qxR3r/ezwXEfWUd64NKsHy88o3UG3QasrgR9e\n" + - "SwEAmCPJHs0LvoU81IFsYhEYaZok9uC0DhdnO2lwYUbCTAXHYQVjh3/jEgAA\n" + - "ADIKKwYBBAGXVQEFAQEHQPz3/CmqzgFI9D6tvzoPlpHQoyKiQ2JWJ4Dtkl2o\n" + - "TnFbAwEIBwAA/01gCk95TUR3XFeibg/u/tVY6a//1q0NWC1X+yui3O24Eb3C\n" + - "jgUYFgoAAAAJBQJjh3/jAhsMAAAAIyIhBRe8+DZtlDb3rzfq2hVsZJRlblqX\n" + - "ac8tXLNF+Lg0NvZS78S6dZamUg5K+sXfU/N1umwTAP9JjPVrtnHjtvYTazZm\n" + - "dZhAn8aRLUtGG1owtmLGwCSh6wD/bNrWG4nHfVk/aEHGZ4cjaFlapFr5t1QS\n" + - "psL7nEy94gs=\n" + - "=5xrR\n" + - "-----END PGP PRIVATE KEY BLOCK-----"; - private static final String CERT = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + - "\n" + - "xjcFY4d/4xYAAAAtCSsGAQQB2kcPAQEHQPlNp7tI1gph5WdwamWH0DMZmbud\n" + - "iRoIJC6thFQ9+JWjwqQFHxYKAAAAHwUCY4d/4wMLCQcFFQoOCAwCFgACGwMC\n" + - "HgkFJwkCBwIAAAAjIiEFF7z4Nm2UNvevN+raFWxklGVuWpdpzy1cs0X4uDQ2\n" + - "9lJ5xSazswKsjQ5+nWIpXpDAYUQBAOd3furFHev97PBcR9ZR3rg0qwfLzyjd\n" + - "QbdBqyuBH15LAQCYI8kezQu+hTzUgWxiERhpmiT24LQOF2c7aXBhRsJMBc48\n" + - "BWOHf+MSAAAAMgorBgEEAZdVAQUBAQdA/Pf8KarOAUj0Pq2/Og+WkdCjIqJD\n" + - "YlYngO2SXahOcVsDAQgHwo4FGBYKAAAACQUCY4d/4wIbDAAAACMiIQUXvPg2\n" + - "bZQ296836toVbGSUZW5al2nPLVyzRfi4NDb2Uu/EunWWplIOSvrF31Pzdbps\n" + - "EwD/SYz1a7Zx47b2E2s2ZnWYQJ/GkS1LRhtaMLZixsAkoesA/2za1huJx31Z\n" + - "P2hBxmeHI2hZWqRa+bdUEqbC+5xMveIL\n" + - "=sVUI\n" + - "-----END PGP PUBLIC KEY BLOCK-----\n"; - - @Test - @Disabled("BC 1.72 does not yet support V5 keys") - public void testParseCert() throws IOException { - PGPPublicKeyRing cert = PGPainless.readKeyRing().publicKeyRing(CERT); - } - - @Test - @Disabled("BC 1.72 does not yet support V5 keys") - public void testParseKey() throws IOException { - PGPSecretKeyRing key = PGPainless.readKeyRing().secretKeyRing(KEY); - } -} 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 7500bab4..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,10 +4,32 @@ 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. @@ -79,4 +101,55 @@ public class WeirdKeys { 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/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 969a0587..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 @@ -24,9 +24,9 @@ import org.pgpainless.algorithm.KeyFlag; import org.pgpainless.algorithm.PublicKeyAlgorithm; 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.Passphrase; @@ -71,9 +71,9 @@ public class BrainpoolKeyGenerationTest { 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")) @@ -99,12 +99,12 @@ public class BrainpoolKeyGenerationTest { PGPSecretKey eddsaSub = iterator.next(); KeyInfo eddsaInfo = new KeyInfo(eddsaSub); - assertEquals(EdDSACurve._Ed25519.getName(), eddsaInfo.getCurveName()); + 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(XDHLegacySpec._X25519.getCurveName(), xdhInfo.getCurveName()); assertEquals(256, xdhSub.getPublicKey().getBitStrength()); PGPSecretKey rsaSub = iterator.next(); 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 7b6710c5..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 @@ -12,7 +12,7 @@ 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.xdh.XDHSpec; +import org.pgpainless.key.generation.type.xdh_legacy.XDHLegacySpec; import org.pgpainless.util.TestAllImplementations; public class CertificationKeyMustBeAbleToCertifyTest { @@ -29,7 +29,7 @@ public class CertificationKeyMustBeAbleToCertifyTest { 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 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 8ea4877d..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 @@ -17,8 +17,8 @@ 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.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; @@ -32,13 +32,13 @@ public class GenerateEllipticCurveKeyTest { 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/GenerateKeyWithCustomCreationDateTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithCustomCreationDateTest.java index d2697b82..0ad564db 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithCustomCreationDateTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithCustomCreationDateTest.java @@ -22,8 +22,8 @@ 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.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.DateUtil; public class GenerateKeyWithCustomCreationDateTest { @@ -33,8 +33,8 @@ public class 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(XDHSpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)) - .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA) + .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(); 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 index 24484cd0..e6a5c96a 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithoutUserIdTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithoutUserIdTest.java @@ -14,7 +14,7 @@ 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.OpenPgpMetadata; +import org.pgpainless.decryption_verification.MessageMetadata; import org.pgpainless.decryption_verification.SignatureVerification; import org.pgpainless.encryption_signing.EncryptionOptions; import org.pgpainless.encryption_signing.EncryptionResult; @@ -22,8 +22,8 @@ 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.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.info.KeyRingInfo; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.timeframe.TestTimeFrameProvider; @@ -47,9 +47,9 @@ public class GenerateKeyWithoutUserIdTest { Date now = new Date(); Date expirationDate = TestTimeFrameProvider.defaultExpirationForCreationDate(now); PGPSecretKeyRing secretKey = PGPainless.buildKeyRing() - .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER).setKeyCreationDate(now)) - .addSubkey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.SIGN_DATA).setKeyCreationDate(now)) - .addSubkey(KeySpec.getBuilder(KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE).setKeyCreationDate(now)) + .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(); @@ -88,10 +88,10 @@ public class GenerateKeyWithoutUserIdTest { Streams.pipeAll(decryptionStream, plaintextOut); decryptionStream.close(); - OpenPgpMetadata metadata = decryptionStream.getResult(); + MessageMetadata metadata = decryptionStream.getMetadata(); - assertTrue(metadata.containsVerifiedSignatureFrom(certificate), - failuresToString(metadata.getInvalidInbandSignatures())); + assertTrue(metadata.isVerifiedSignedBy(certificate), + failuresToString(metadata.getRejectedInlineSignatures())); assertTrue(metadata.isEncrypted()); } 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 24ea4aa4..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 @@ -10,8 +10,8 @@ import org.junit.jupiter.api.TestTemplate; import org.junit.jupiter.api.extension.ExtendWith; import org.pgpainless.algorithm.KeyFlag; 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 { @@ -20,18 +20,18 @@ public class IllegalKeyFlagsTest { @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 index 231b0485..238d1a40 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/generation/KeyGenerationSubpacketsTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/generation/KeyGenerationSubpacketsTest.java @@ -30,8 +30,8 @@ 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.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.info.KeyRingInfo; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.signature.subpackets.SelfSignatureSubpackets; @@ -113,7 +113,7 @@ public class KeyGenerationSubpacketsTest { List keysBefore = info.getPublicKeys(); secretKeys = PGPainless.modifyKeyRing(secretKeys) - .addSubKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.SIGN_DATA).build(), + .addSubKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.SIGN_DATA).build(), Passphrase.emptyPassphrase(), SecretKeyRingProtector.unprotectedKeys()) .done(); @@ -130,7 +130,7 @@ public class KeyGenerationSubpacketsTest { assertNotNull(bindingSig.getHashedSubPackets().getEmbeddedSignatures().get(0)); secretKeys = PGPainless.modifyKeyRing(secretKeys) - .addSubKey(KeySpec.getBuilder(KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_COMMS).build(), + .addSubKey(KeySpec.getBuilder(KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.ENCRYPT_COMMS).build(), Passphrase.emptyPassphrase(), new SelfSignatureSubpackets.Callback() { @Override 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 61e09477..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 @@ -45,7 +45,7 @@ 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; @@ -149,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(); @@ -224,7 +224,7 @@ public class KeyRingInfoTest { 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)) @@ -269,10 +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); // 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); } @@ -557,7 +560,7 @@ public class KeyRingInfoTest { public void testGetExpirationDateForUse_NoSuchKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { 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); 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 fd665901..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; @@ -41,10 +41,10 @@ public class UserIdRevocationTest { public void testRevocationWithoutRevocationAttributes() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { 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(); @@ -79,9 +79,9 @@ public class UserIdRevocationTest { public void testRevocationWithRevocationReason() throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { 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/AddSubkeyWithModifiedBindingSignatureSubpackets.java b/pgpainless-core/src/test/java/org/pgpainless/key/modification/AddSubkeyWithModifiedBindingSignatureSubpackets.java index 77143b9f..85cddfd6 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/modification/AddSubkeyWithModifiedBindingSignatureSubpackets.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/modification/AddSubkeyWithModifiedBindingSignatureSubpackets.java @@ -6,6 +6,7 @@ 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; @@ -28,7 +29,7 @@ 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.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.signature.subpackets.SelfSignatureSubpackets; @@ -46,7 +47,7 @@ public class AddSubkeyWithModifiedBindingSignatureSubpackets { KeyRingInfo before = PGPainless.inspectKeyRing(secretKeys); PGPKeyPair secretSubkey = KeyRingBuilder.generateKeyPair( - KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.SIGN_DATA).build()); + KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.SIGN_DATA).build()); long secondsUntilExpiration = 1000; secretKeys = PGPainless.modifyKeyRing(secretKeys) @@ -65,10 +66,12 @@ public class AddSubkeyWithModifiedBindingSignatureSubpackets { 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, - after.getSubkeyExpirationDate(new OpenPgpV4Fingerprint(newKey)).getTime(), 2 * MILLIS_IN_SEC); + newExpirationDate.getTime(), 2 * MILLIS_IN_SEC); assertTrue(newKey.getSignatures().hasNext()); PGPSignature binding = newKey.getSignatures().next(); List notations = SignatureSubpacketsUtil.getHashedNotationData(binding); 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 19f84930..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 @@ -12,9 +12,11 @@ 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.api.Test; @@ -113,16 +115,17 @@ public class AddUserIdTest { @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) + secretKeys = PGPainless.modifyKeyRing(secretKeys, DateExtensionsKt.plusSeconds(now, 1)) .addPrimaryUserId(bob, SecretKeyRingProtector.unprotectedKeys()) .done(); - assertEquals("Bob", PGPainless.inspectKeyRing(secretKeys).getPrimaryUserId()); + 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 9acb08f6..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 @@ -155,7 +155,7 @@ 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.toSecondsPrecision(expirationDate); 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 index e0429287..d3fe1b2e 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/modification/FixUserIdDoesNotBreakEncryptionCapabilityTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/modification/FixUserIdDoesNotBreakEncryptionCapabilityTest.java @@ -4,22 +4,11 @@ package org.pgpainless.key.modification; -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.OpenPgpMetadata; -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; +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; @@ -27,10 +16,23 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; 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.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; +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. @@ -130,7 +132,9 @@ public class FixUserIdDoesNotBreakEncryptionCapabilityTest { assertTrue(info.isUserIdValid(userIdAfter)); assertEquals(userIdAfter, info.getPrimaryUserId()); - assertTrue(info.getLatestUserIdCertification(userIdAfter).getHashedSubPackets().isPrimaryUserID()); + PGPSignature latestCertification = info.getLatestUserIdCertification(userIdAfter); + assertNotNull(latestCertification); + assertTrue(latestCertification.getHashedSubPackets().isPrimaryUserID()); PGPPublicKeyRing cert = PGPainless.extractCertificate(edited); @@ -156,7 +160,7 @@ public class FixUserIdDoesNotBreakEncryptionCapabilityTest { Streams.pipeAll(decryptionStream, plain); decryptionStream.close(); - OpenPgpMetadata metadata = decryptionStream.getResult(); + 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/OldSignatureSubpacketsArePreservedOnNewSigTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/modification/OldSignatureSubpacketsArePreservedOnNewSigTest.java index 674cd6e5..b8c3244e 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/modification/OldSignatureSubpacketsArePreservedOnNewSigTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/modification/OldSignatureSubpacketsArePreservedOnNewSigTest.java @@ -5,8 +5,8 @@ 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.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; @@ -34,9 +34,10 @@ public class OldSignatureSubpacketsArePreservedOnNewSigTest { .simpleEcKeyRing("Alice "); PGPSignature oldSignature = PGPainless.inspectKeyRing(secretKeys).getLatestUserIdCertification("Alice "); + assertNotNull(oldSignature); PGPSignatureSubpacketVector oldPackets = oldSignature.getHashedSubPackets(); - assertEquals(0, oldPackets.getKeyExpirationTime()); + long oldExpiration = oldPackets.getKeyExpirationTime(); Date now = new Date(); Date t1 = new Date(now.getTime() + millisInHour); @@ -46,9 +47,10 @@ public class OldSignatureSubpacketsArePreservedOnNewSigTest { .setExpirationDate(expiration, new UnprotectedKeysProtector()) .done(); 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 index 73c5953c..04197d6f 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/modification/RefuseToAddWeakSubkeyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/modification/RefuseToAddWeakSubkeyTest.java @@ -67,16 +67,17 @@ public class RefuseToAddWeakSubkeyTest { 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); + minimalBitStrengths.put(PublicKeyAlgorithm.EDDSA_LEGACY, 250); // §7.2.1 minimalBitStrengths.put(PublicKeyAlgorithm.DIFFIE_HELLMAN, 2000); // §7.2.2 minimalBitStrengths.put(PublicKeyAlgorithm.ECDH, 250); - minimalBitStrengths.put(PublicKeyAlgorithm.EC, 250); PGPainless.getPolicy().setPublicKeyAlgorithmPolicy(new Policy.PublicKeyAlgorithmPolicy(minimalBitStrengths)); SecretKeyRingEditorInterface editor = PGPainless.modifyKeyRing(secretKeys); - KeySpec spec = KeySpec.getBuilder(KeyType.RSA(RsaLength._1024), KeyFlag.ENCRYPT_COMMS).build(); + 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(); 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 index ec518847..48b5d5b7 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/modification/RevocationCertificateTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/modification/RevocationCertificateTest.java @@ -4,13 +4,20 @@ 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; @@ -19,6 +26,7 @@ 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 { @@ -27,7 +35,7 @@ public class RevocationCertificateTest { PGPSecretKeyRing secretKeys = TestKeys.getEmilSecretKeyRing(); PGPSignature revocation = PGPainless.modifyKeyRing(secretKeys) - .createRevocationCertificate(SecretKeyRingProtector.unprotectedKeys(), + .createRevocation(SecretKeyRingProtector.unprotectedKeys(), RevocationAttributes.createKeyRevocation() .withReason(RevocationAttributes.Reason.KEY_RETIRED) .withoutDescription()); @@ -43,4 +51,60 @@ public class RevocationCertificateTest { 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/RevokeSubKeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/modification/RevokeSubKeyTest.java index 6a044199..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 @@ -75,7 +75,7 @@ public class RevokeSubKeyTest { 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.")); @@ -98,8 +98,8 @@ public class RevokeSubKeyTest { .forKey(secretKeys, Passphrase.fromPassword("password123")); SecretKeyRingEditorInterface editor = PGPainless.modifyKeyRing(secretKeys); - PGPSignature keyRevocation = editor.createRevocationCertificate(primaryKey.getKeyID(), protector, (RevocationAttributes) null); - PGPSignature subkeyRevocation = editor.createRevocationCertificate(subKey.getKeyID(), protector, (RevocationAttributes) 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()); 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 index 9dc968ed..fb6f1ec1 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/modification/RevokeUserIdsTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/modification/RevokeUserIdsTest.java @@ -5,6 +5,7 @@ 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; @@ -81,6 +82,7 @@ public class RevokeUserIdsTest { assertTrue(info.isUserIdValid("Alice ")); PGPSignature revocation = info.getUserIdRevocation("Allice "); + assertNotNull(revocation); assertFalse(RevocationAttributes.Reason.isHardRevocation( revocation.getHashedSubPackets().getRevocationReason().getRevocationReason())); 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 8a634200..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 @@ -112,7 +112,7 @@ class KeyRingReaderTest { } /** - * One armored pub key + * One armored pub key. */ @Test void parsePublicKeysSingleArmored() throws IOException, PGPException { @@ -120,7 +120,7 @@ class KeyRingReaderTest { } /** - * One binary pub key + * One binary pub key. */ @Test void parsePublicKeysSingleBinary() throws IOException, PGPException { @@ -128,7 +128,7 @@ class KeyRingReaderTest { } /** - * 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 { @@ -136,7 +136,7 @@ class KeyRingReaderTest { } /** - * 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 { @@ -153,7 +153,7 @@ class KeyRingReaderTest { } /** - * Many binary pub keys + * Many binary pub keys. */ @Test void parsePublicKeysMultiplyBinary() throws IOException, PGPException { @@ -161,7 +161,7 @@ class KeyRingReaderTest { } /** - * One armored private key + * One armored private key. */ @Test void parseSecretKeysSingleArmored() throws IOException, PGPException { @@ -169,7 +169,7 @@ class KeyRingReaderTest { } /** - * One binary private key + * One binary private key. */ @Test void parseSecretKeysSingleBinary() throws IOException, PGPException { @@ -177,7 +177,7 @@ class KeyRingReaderTest { } /** - * 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 @@ -186,7 +186,7 @@ class KeyRingReaderTest { } /** - * 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 { @@ -203,7 +203,7 @@ class KeyRingReaderTest { } /** - * Many binary private keys + * Many binary private keys. */ @Test void parseSecretKeysMultiplyBinary() throws IOException, PGPException { @@ -211,7 +211,7 @@ class KeyRingReaderTest { } /** - * 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 { @@ -219,7 +219,7 @@ class KeyRingReaderTest { } /** - * 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 @@ -228,7 +228,7 @@ class KeyRingReaderTest { } /** - * Many binary keys(private or pub) + * Many binary keys(private or pub). */ @Test void parseKeysMultiplyBinary() throws IOException, PGPException { 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 d1f5f9e6..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 @@ -129,7 +129,8 @@ public class CachingSecretKeyRingProtectorTest { Passphrase passphrase = withCallback.getPassphraseFor(x); assertNotNull(passphrase); - assertEquals(doubled, (long) Long.valueOf(new String(passphrase.getChars()))); + assertNotNull(passphrase.getChars()); + assertEquals(doubled, Long.parseLong(new String(passphrase.getChars()))); } } 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/fixes/S2KUsageFixTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/protection/fixes/S2KUsageFixTest.java index f324892f..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 @@ -84,7 +84,7 @@ public class S2KUsageFixTest { } PGPSecretKeyRing after = PGPainless.modifyKeyRing(unprotected) - .changePassphraseFromOldPassphrase(null) + .changePassphraseFromOldPassphrase(Passphrase.emptyPassphrase()) .withSecureDefaultSettings() .toNewPassphrase(Passphrase.fromPassword("after")) .done(); 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/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 aa7078e4..9ff4df85 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/policy/PolicyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/policy/PolicyTest.java @@ -44,7 +44,7 @@ public class PolicyTest { sigHashAlgoMap.put(HashAlgorithm.SHA256, null); sigHashAlgoMap.put(HashAlgorithm.SHA224, null); sigHashAlgoMap.put(HashAlgorithm.SHA1, DateUtil.parseUTCDate("2013-02-01 00:00:00 UTC")); - policy.setSignatureHashAlgorithmPolicy(new Policy.HashAlgorithmPolicy(HashAlgorithm.SHA512, sigHashAlgoMap)); + policy.setCertificationSignatureHashAlgorithmPolicy(new Policy.HashAlgorithmPolicy(HashAlgorithm.SHA512, sigHashAlgoMap)); Map revHashAlgoMap = new HashMap<>(); revHashAlgoMap.put(HashAlgorithm.SHA512, null); @@ -107,40 +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.getSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA1, DateUtil.parseUTCDate("2000-01-01 00:00:00 UTC"))); - assertTrue(policy.getSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA1.getAlgorithmId(), DateUtil.parseUTCDate("2000-01-01 00:00:00 UTC"))); + 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.getSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA1, DateUtil.parseUTCDate("2020-01-01 00:00:00 UTC"))); - assertFalse(policy.getSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA1.getAlgorithmId(), DateUtil.parseUTCDate("2020-01-01 00:00:00 UTC"))); + 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.getSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA1, DateUtil.parseUTCDate("2000-01-01 00:00:00 UTC"))); - assertTrue(policy.getSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA1.getAlgorithmId(), DateUtil.parseUTCDate("2000-01-01 00:00:00 UTC"))); + 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.getSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA1, DateUtil.parseUTCDate("2020-01-01 00:00:00 UTC"))); - assertFalse(policy.getSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA1.getAlgorithmId(), DateUtil.parseUTCDate("2020-01-01 00:00:00 UTC"))); + 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 @@ -181,8 +181,8 @@ public class PolicyTest { @Test public void testUnknownSignatureHashAlgorithmIsNotAcceptable() { - assertFalse(policy.getSignatureHashAlgorithmPolicy().isAcceptable(-1)); - assertFalse(policy.getSignatureHashAlgorithmPolicy().isAcceptable(-1, new Date())); + assertFalse(policy.getCertificationSignatureHashAlgorithmPolicy().isAcceptable(-1)); + assertFalse(policy.getCertificationSignatureHashAlgorithmPolicy().isAcceptable(-1, new Date())); } @Test 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" + + "lQcYBGSHDHcBEACKzJaLOQAG36oZPUJmes0YmbLe8oZt33hIaRLxj7mI4oOqOlMp\n" + + "nyYKhg4KmneFwXkYLe9/LlT194ZDVQi1qYR91DUNsM9sTWNIVEq8Bk88Rc8zSg8r\n" + + "R6QcADNf+P9cz/NlR8pcet89CP84WrytEFQe942APKSvkOejOSdJZRYo6aV25N/0\n" + + "Oh3m16T09JHEBc6SkkutjJ9sp7x/bHSqRZrUIyFJsf3zL3o3u+cQ0sO0h2nIt4zW\n" + + "Z7q58UNka1+CUy0FRaYMtjLOou3OEI2YmewMPzeam9qpCMn31fxZRA8VEDJFFjYq\n" + + "8KbjsdxeTVMvPwbMaKQrAd6ekLQpbRVc5SEQngySDL3fkZoOksyZlb02TDk2/yiG\n" + + "VkATMvJEf0WQ2PXCJoS1sJ++rSIHK/ZwAfofU4ScZcoZ2A+KAm6cGmCOsTuARKsS\n" + + "aon0JIxAEIbTWWon1bZjwdWreGRpIotBabOVmJI02VPAtoCwBUrQSraROmbaZiwh\n" + + "Veitcbsnw8kksnQW2WU6isxe9+wcz4zoy4I0/7LO9PYfF0QX1HidhuY6dh0OoDPs\n" + + "wXf9jnefSbOhDsB2e8DnAd1TBkQi2CbNDneCgwkqUefue4kAOQAdkKSurruiWVfR\n" + + "pUx6h1zGiZd9Xx9KBqW8lcGFuQkmNBf2wPAaGOoHWNYK+3Cjg9/JjYV/aQARAQAB\n" + + "AA/9HRuqtqfNiaN+WKywMC2wtWgGSxsxdflXkFf40RADKOHYGusAZcoqgCp851xd\n" + + "lH9uldOMVm+xAaRXU+eRXAeg8YJ/Xc+msF/KYnDK+4OXOlyph2gQplLv569jFs1x\n" + + "QFWcBbF4jsx15KbuXzMoPmMeQSBJXItbPjZ1XZWu14WLkCjvum9lSCKoArFWtshT\n" + + "iXhuSe7EUDEdffvvXAmyrcLLJZH4eSwKxeU/DFErtCt/P2zmkmFr98rpLfWa3IRo\n" + + "Ezy+REzG9gN/xDYHCSAPkeE1z5uyXYox4SEmugL2WQIc553t7O8rjPXLChVS0bQF\n" + + "rfLZyiRnyV0rArX2Enn98O1ccYCi/7zh3UDfWVp4QeYCajxLMusCgwY7CNAarnrG\n" + + "5ue7D5rI4PLzmWx2q/HI++GANEIyUFoDwZ8GH49WnYZJx3m1CuDy2H8NaALOXamm\n" + + "uVN3ZQekrIDLA9SodfXt9yxvRb48Gi6VDvgVEezcgY4HT9wEkLBjqFxEcu+owKso\n" + + "7Sr2Q3oYij+aIL+QWUD3WGyCUhOlurWu0nvlMsGrm2CYq3Scnf4wlz/bSSQ5USwO\n" + + "clyaNck41I34BqULljbQn651LV2a++M74DeZapy0RgT1ShYYaBA6oQ/OVuIrPEHJ\n" + + "rZgUlt/tBtL6xyDKI43/VGQzb6/J6byDsCWIpgRnUVICbsEIALs2aXjrau3wMb95\n" + + "gK5qjNRNnaLDRNjkb1Vf8jJOsT2s5/J3K6uOEO1XB26BFQzQbQ+JrqKKD3NfuRY2\n" + + "g2pk+IiU46r3/xEOQMKO9MS5AlAN5y7y34OtPAj7uchrbUShrDy26UvOBIaZddNq\n" + + "gSGn8lsQCE0CFGX/4ke48TsK9GSocYfdgEECAPzcUdu1ws8PnxhfNLoBRBl/PD/W\n" + + "gfIsNNuMJyA7KixjgT6DKSjFRHPVxSa990ewqtCa10Eqwn5DGM10bDymKQIpkj1j\n" + + "0bbNNT7sP7ZPrinD05h2sr2AfzIxAgeB6BCd4m761BBpUB+hDVtl+kgDIc+kQp+P\n" + + "8ib4AFsIAL3MURxJLVx/8v6B9EeA2cWaQQf0V62sa+2ZgBgdSW3y4ryvvfP/RNg6\n" + + "majNnOxLkD8W5TA/0ujdJERRfJUGW2I71bV1NZYrZsmU+mIS5uZ6fg/IzcCNFIt9\n" + + "8dc4UgOSUWogk8XczYaNViw+Nx9a9AtdI/Y3AB+m8iDZrRLYjrRAM3fARU/3qObG\n" + + "nQhkiI8r+x6c8jtf/aj86SlgTNM8VKFFKJCv8RC2JLQLTvzgrqczETscAvjSkZ1U\n" + + "vuEQe9QQIdWcnLzl6ql5BKreG7KXHqBXUTDW8OwyZkDQpi5TgK0KBEMEHTywtiWs\n" + + "fTNschzXeJBV+nf8sinYriJZkcm5SosH/0ojJRXc/yW2UZt01IE48tze2rPGYzyl\n" + + "JH5dtNkh5nRtUaycdi35rjYYxL23wcZ33epxV6tWs2lTcLc4b4GS9sCR5zl+98V7\n" + + "iu/+CXVOMz9Eo6LXBHr/QX6Pgkwct4ygDYdexcHMMJnn7HbprVvmTEiKHBVb823r\n" + + "nXbu2myeNea8yVoQDk9uiOjdp7SB2lr4QZ6ai9jC1WZsJ1JYBV3ndssJ3taMRpKX\n" + + "Kv3gwgR+MxLOdde1Bf+6RGCOW9ZrAqZvBycXXxppsUwg8rxl4LPd61Mb7QLpsqZg\n" + + "m3XkCI+NbL4YsHyr4cQBnJg7qRFWBvO+yDwav3ymM5ZGTrxnbrqfF/6D6rQcQWxp\n" + + "Y2UgPGFsaWNlQHBncGFpbmxlc3Mub3JnPokCUwQTAQoARwUCZIcMdwkQkNyaARSW\n" + + "gkIWIQSI+xP0LaaEgFc9IAuQ3JoBFJaCQgKeAQKbAQUWAgMBAAQLCQgHBRUKCQgL\n" + + "BYkJZgF/ApkBAAChRg//VVpi93b0gwsuzqTxjHF9C8KyWW5POV8JGRedtUs6qXly\n" + + "Q6hy2ZiBkMJWJnZByKjJXhayLeHAVQXQtY+8Cy5YbdzzqbQ9I7puQvw1Kf+DQKnr\n" + + "FbxzxaPMdAnQgAfaS+0WhXLBz3OYonUCCiqRAQlWb0rGJz4H8hsGQRATVjCgSGnU\n" + + "cE7rOiJ4JzqyOHQZY+EbZ89rngE7whn2T4j/9728girM6Zy519dyZKe3Gma9/IPj\n" + + "WAs+94m5dw3EzaJcyWbxB333Gg1CSsFaa3j30TuxmdxuWzkJ4SI6L8j9ppeMYPeF\n" + + "a/NW3VveUO6PqXEOThcxwOWvQp0lz5FoT7o+9VQ1lbKJGbpwtYgJ9IySx2/P64pF\n" + + "scUXXERz8Cfh5WwnpzHVUp/NHEuVqjU5dRlhXwgL66KiRrnbxBquifRtu0rgyVop\n" + + "SgCkapAinvBcnLIjIiKJn9KpSzvzh1psyAr95mCCzRKhJfgA93+fGYIHl1xi2tsO\n" + + "WsjU7MQcH+ZftRZdCmgmnxpn7z5TCVqM4zMezAEwRv0AmW0SdTTGri6fA/EKOQPS\n" + + "9Uf2yRr9gveh0Xr1ohHVloyh7fXt6rNX9SuOSZ8NLETELj9mcligVH+iPOUhobiy\n" + + "MSh+EE346g9HRXRm/16tARQFGuaGu7+w+IrBoRJKM9Kghfc5gDyy7tQox2QyP3ad\n" + + "AdgEZIcMdwEEAMuBMOMAhNBgya5lS9dRV4+mOWZF6GG8VG/Kgl0slTrm+rOJeZPR\n" + + "QHDEJB5JXUM57UklX4Uf0IupZkcnfuf0vm5KERMg6l3XnXg6DIBiQze3QnpJJSWz\n" + + "9aMCx+TkG9R0A2htSe2y55XvaEMxOKUa8vJjk8SKX2mlEONd8xPBZ2k9ABEBAAEA\n" + + "A/0S5SLp4NF0G5h+Rc995YKQxulLcrA56ueYHaBvEEWm221poBkWvXYTQlLsMPmk\n" + + "1UsP4JV24dZWMVtavfMNjG7l2irWKJq7YmpkjJypJ+BYPGUfM0MISt/blZhxZRRm\n" + + "xSUsADd8VXfARRdxLoDE2vNSXkX+y5PDffOHho6llSx1DQIA1xZPteS2rFL/0FO/\n" + + "MaNyjErpYVomg1M6LMON3XQDR6YYndum4uQNgj7IqSbE8iJDU9uGWRC44EQZoSUS\n" + + "NY90BwIA8jbeXeaQMTtz3oBDdc8t2cFwKIepE0NEuGXm+Eaj4dwbSiwpEg+pCa1R\n" + + "DVoNxp1i43IxQLlLbrjz44Ch19JPmwH/WHTni5h/8kszv/Ra9G6irjq2SQtnZ5m9\n" + + "b2h2snUizKebCmTQ0wZlfLpGHGFnWgpkf882QZ/rPzr8jk/TKQ4F4J6NiQLRBBgB\n" + + "CgC7BQJkhwx3Ap4BApsCBRYCAwEABAsJCAcFFQoJCAudIAQZAQoABgUCZIcMdwAK\n" + + "CRAdzMPWl9+PZqgmA/938qa58LMuj+17YYj7QKeJ+YojJsDiwm0kpAqcUKvODQw+\n" + + "65EpTjrktyX/jhya09Ag9mBQklFJfIGA9C4eWHBfVIwulYFyPyBtHtFMJXLacXoW\n" + + "h3d7eYsay62nHQKEiiwCULnL/yoQc1gapSYu7/7J6kBk+1y1KJUG867PWq3QtgAK\n" + + "CRCQ3JoBFJaCQtsWD/kBE2w/EZrvPBEl96POfQwOJL3zsozZ+QEEvPKOAsXfoaMK\n" + + "Ff3i1HF3ts/NYHfeT0kHEsVMcODEfQv5LbbcF6BLeusDkuNEg1N8l/LwjN9UnrQW\n" + + "biQjpEHafETcocvr9XwVYKx4+rGQ3SGvqxb6iiVKHGBprFbeqhQdASRTP9zYMI/4\n" + + "eZnXADrfGGJt2P7AaDzcKCSOH/E9KQYROoDFsHYhyX/DFZZsMX8lNRfTDjfsu49+\n" + + "J+xcSUrFKYYpa3DPS5O+Q9glBFPeeLq7LiqE5c7XwrJtJUcurxMswcv7y3b1fQW2\n" + + "+fR3ucmT/3C6n8SSXbPBxoRaK1xBwwXbjSrhVjl8/yrjZmxnQEpIbe5eFvy1WAHH\n" + + "vdUfRDlD/IRczmkSfeTet3YAWqs5uSNwc+wo0zpPzyrHSWdB/5SXfgo/pPugdhSp\n" + + "LDFM1SSHtthgiMVxcZD0yE5YK81PMI9SXRcG608opGPCLnIiYoN21rbuF/z6tITk\n" + + "SVKLbbOJ/M3lV0elIBWLUl7UPNL0Ss+yvLpezwx7Bx/GCEsA7N5RMy9q1wSKhUmb\n" + + "mT0SdVwM1BztRXESIxilA61uob8wUPxtPFBlMH0tSUtfnInWi45y3W0YmGQsLNYQ\n" + + "Sr99oL5LLHFVUbkP8boo6O65YkKGV0tDbW6J6Nk7NOquCFjuhNH1UffnEPSD4Z0B\n" + + "2ARkhwx3AQQAizGy1gpu9cK2iPefbK5U+/0WvQNf+VMt/k0cr7vd2SP6pAaPHWoC\n" + + "QDigYcv3DsvIWSLvMUAcVKZ7kysf8kPfrceoPqJjw8B5kxV80QYJ9V9efXpJ2ALb\n" + + "kmggXoKyW8cink6TS2HkeF2q4pwgrG6Jcc5TlNIPMYVuu0qnPuFTRNkAEQEAAQAD\n" + + "/R3pMRfjqBJxpA3swDp3f9WifjdhuUb3g+OoqGhvA2482AhVTH2yoQcx4+0ACngI\n" + + "fGYDJsk7kJr0J4vpcXT69t6s3gH/uOO+MFHQa6u6qRejKcNOW8qnENDB9PN81Nu3\n" + + "wbiNn5+BnjPSl9H6rL6Lqh5qk9X57gR2l6ty8EtZHntxAgC1U9rFbopj9AGhAMdr\n" + + "qc5koUUjvyrXAsZlqCeyxRipf1M3UEMCgETa8DWcFGZkotYJCf7kNXqeRS/9nkpP\n" + + "9PB9AgDEhAAk0T3QRxvUi9Aw6RApVxHVv8rNzcjHMlR7idrtQiUObJXK61P67aLn\n" + + "BZ3QApneE9wZqM8DlxVsj2mSXxCNAgCi6FJY/uSHYA8NL2KDzKrdGtW3YdLcr8Ud\n" + + "LUAR3YTLTzFXUPaxQ1nkPcSkVVd/6LQ10VmwioTvMQP/Vs+gcB36p0eJAjMEGAEK\n" + + "AB0FAmSHDHcCngECmwwFFgIDAQAECwkIBwUVCgkICwAKCRCQ3JoBFJaCQj+lD/42\n" + + "iRWNTDKg+gBhz1ZBrt2VR8C4Rm8g1HyJ1SWeftLI5PmgfqVQUYG2JfCwaAyBQrrd\n" + + "UExxdYeEv9hjYDDPRl/ZFMzKLGZxxPs0s7I32tiMYrMrJYxDqc/cmO1siFWisgT9\n" + + "KkDs8vZbLTVEPy0cD/xg2M6N/atHD90HqPmi+qwuote328zR5piX7GThtVwJnYGN\n" + + "rfqRWsunHk+dvw4g2gjw9hwHafzqJ2APhWLi3wmIUAVjbJLl25Zkaxnm2zkj85sp\n" + + "Z1Ly9cjJlmlV9/N+762lwtvEGno7Umi5NdXtwqQVN6vHVQEvS5cypwcfO9FnLXh0\n" + + "n+XqaEtp5EEQJOqEjsw1a/l3HpqteexsjXoGdPKmDY4xqMDduYXcklgg9vfTdbGb\n" + + "CslbUFLmJjzMBeukhS/51r39FmmBpQF9zC7U//hKMQy6rsI/gSnPMT3c7oMNZAbo\n" + + "q+9HSfMtt6SriQBg9TUn+QnrQotJKpXh1gX/rqkAz7azmlNcvSI5TTcBM74LcsWg\n" + + "ntlUcNI96UYFyiiBzYjESdCinQFlBwSKKtSvLUFrrANmmkTZHFCrE1Ig/w6vDXoN\n" + + "rsFTjG7e6Mz/uEeSA3e0n/xk7/6WPQDCYP/3qRGErAgPNK5zVavHI0Bzw+A/DP7s\n" + + "fr7HL/cch3YQK5AKifvyATqF72k7BjrFETP6Ly4X3A==\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 60c0bc19..5489a11c 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/provider/ProviderFactoryTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/provider/ProviderFactoryTest.java @@ -21,14 +21,10 @@ public class ProviderFactoryTest { }; @Override - protected Provider _getProvider() { + protected Provider getSecurityProvider() { return provider; } - @Override - protected String _getProviderName() { - return provider.getName(); - } }; @Test 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 a17f700e..175ce101 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/signature/CertificateValidatorTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/CertificateValidatorTest.java @@ -24,7 +24,7 @@ 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.policy.Policy; import org.pgpainless.signature.consumer.CertificateValidator; @@ -1391,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/IgnoreMarkerPacketsTest.java b/pgpainless-core/src/test/java/org/pgpainless/signature/IgnoreMarkerPacketsTest.java index 6ece093b..ff26506d 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/signature/IgnoreMarkerPacketsTest.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; @@ -154,8 +154,8 @@ public class IgnoreMarkerPacketsTest { 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 IgnoreMarkerPacketsTest { 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/OnePassSignatureBracketingTest.java b/pgpainless-core/src/test/java/org/pgpainless/signature/OnePassSignatureBracketingTest.java index 0042d14b..fd7c53e9 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/signature/OnePassSignatureBracketingTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/OnePassSignatureBracketingTest.java @@ -144,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/SignatureStructureTest.java b/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureStructureTest.java index cf118822..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; @@ -80,7 +82,9 @@ public class SignatureStructureTest { @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 d5cfb4f5..1caeb9e9 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureSubpacketsUtilTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureSubpacketsUtilTest.java @@ -140,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 @@ -216,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()); } @@ -277,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/builder/ThirdPartyCertificationSignatureBuilderTest.java b/pgpainless-core/src/test/java/org/pgpainless/signature/builder/ThirdPartyCertificationSignatureBuilderTest.java index ced6a466..2b0f4d35 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/signature/builder/ThirdPartyCertificationSignatureBuilderTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/builder/ThirdPartyCertificationSignatureBuilderTest.java @@ -4,6 +4,7 @@ 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; @@ -22,6 +23,7 @@ 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; @@ -61,10 +63,12 @@ public class ThirdPartyCertificationSignatureBuilderTest { assertEquals(SignatureType.GENERIC_CERTIFICATION, SignatureType.valueOf(certification.getSignatureType())); assertEquals(secretKeys.getPublicKey().getKeyID(), certification.getKeyID()); assertArrayEquals(secretKeys.getPublicKey().getFingerprint(), certification.getHashedSubPackets().getIssuerFingerprint().getFingerprint()); - assertFalse(SignatureSubpacketsUtil.getExportableCertification(certification).isExportable()); + Exportable exportable = SignatureSubpacketsUtil.getExportableCertification(certification); + assertNotNull(exportable); + assertFalse(exportable.isExportable()); // test sig correctness - certification.init(ImplementationFactory.getInstance().getPGPContentVerifierBuilderProvider(), secretKeys.getPublicKey()); + 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 index 72acc125..56605f83 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/signature/builder/ThirdPartyDirectKeySignatureBuilderTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/builder/ThirdPartyDirectKeySignatureBuilderTest.java @@ -8,8 +8,6 @@ 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.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; import java.util.Collections; import java.util.Date; @@ -33,7 +31,7 @@ import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil; public class ThirdPartyDirectKeySignatureBuilderTest { @Test - public void testDirectKeySignatureBuilding() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + public void testDirectKeySignatureBuilding() throws PGPException { PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() .modernKeyRing("Alice"); @@ -55,7 +53,7 @@ public class ThirdPartyDirectKeySignatureBuilderTest { } }); - PGPSignature directKeySig = dsb.build(secretKeys.getPublicKey()); + PGPSignature directKeySig = dsb.build(); assertNotNull(directKeySig); secretKeys = KeyRingUtils.injectCertification(secretKeys, secretKeys.getPublicKey(), directKeySig); 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 index 988bc776..14eed4de 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/signature/subpackets/SignatureSubpacketsTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/subpackets/SignatureSubpacketsTest.java @@ -376,7 +376,7 @@ public class SignatureSubpacketsTest { public void testSetSignatureTarget() { byte[] hash = new byte[20]; new Random().nextBytes(hash); - wrapper.setSignatureTarget(PublicKeyAlgorithm.fromId(key.getAlgorithm()), HashAlgorithm.SHA512, hash); + wrapper.setSignatureTarget(PublicKeyAlgorithm.requireFromId(key.getAlgorithm()), HashAlgorithm.SHA512, hash); PGPSignatureSubpacketVector vector = SignatureSubpacketsHelper.toVector(wrapper); SignatureTarget target = vector.getSignatureTarget(); @@ -388,7 +388,7 @@ public class SignatureSubpacketsTest { @Test public void testSetFeatures() { - wrapper.setFeatures(Feature.MODIFICATION_DETECTION, Feature.AEAD_ENCRYPTED_DATA); + wrapper.setFeatures(Feature.MODIFICATION_DETECTION, Feature.LIBREPGP_OCB_ENCRYPTED_DATA); PGPSignatureSubpacketVector vector = SignatureSubpacketsHelper.toVector(wrapper); Features features = vector.getFeatures(); @@ -476,7 +476,7 @@ public class SignatureSubpacketsTest { 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.AEAD_ENCRYPTED_DATA)); + 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); 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 5132ef57..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 @@ -35,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); @@ -49,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 d3c503ab..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 @@ -53,7 +53,7 @@ public class SymmetricEncryptionTest { .onOutputStream(ciphertextOut) .withOptions(ProducerOptions.encrypt( EncryptionOptions.encryptCommunications() - .addPassphrase(encryptionPassphrase) + .addMessagePassphrase(encryptionPassphrase) .addRecipient(encryptionKey) )); @@ -66,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(); @@ -103,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(); @@ -112,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/util/ArmorUtilsTest.java b/pgpainless-core/src/test/java/org/pgpainless/util/ArmorUtilsTest.java index b83c0d03..c0a6e00c 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/util/ArmorUtilsTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/util/ArmorUtilsTest.java @@ -96,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")); diff --git a/pgpainless-core/src/test/java/org/pgpainless/util/ArmoredInputStreamFactoryTest.java b/pgpainless-core/src/test/java/org/pgpainless/util/ArmoredInputStreamFactoryTest.java index 147b957d..95e3961b 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/util/ArmoredInputStreamFactoryTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/util/ArmoredInputStreamFactoryTest.java @@ -11,8 +11,6 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertTrue; public class ArmoredInputStreamFactoryTest { @@ -30,22 +28,4 @@ public class ArmoredInputStreamFactoryTest { ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(inputStream); assertNotNull(armorIn); } - - @Test - public void testGet_willWrapArmoredInputStreamWithCRC() throws IOException { - ByteArrayInputStream inputStream = new ByteArrayInputStream(armored.getBytes()); - ArmoredInputStream plainArmor = new ArmoredInputStream(inputStream); - - ArmoredInputStream armor = ArmoredInputStreamFactory.get(plainArmor); - assertTrue(armor instanceof CRCingArmoredInputStreamWrapper); - } - - @Test - public void testGet_onCRCinArmoredInputStream() throws IOException { - ByteArrayInputStream inputStream = new ByteArrayInputStream(armored.getBytes()); - CRCingArmoredInputStreamWrapper crc = new CRCingArmoredInputStreamWrapper(new ArmoredInputStream(inputStream)); - - ArmoredInputStream armor = ArmoredInputStreamFactory.get(crc); - assertSame(crc, armor); - } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/CollectionUtils.java b/pgpainless-core/src/test/java/org/pgpainless/util/CollectionUtils.java similarity index 82% rename from pgpainless-core/src/main/java/org/pgpainless/util/CollectionUtils.java rename to pgpainless-core/src/test/java/org/pgpainless/util/CollectionUtils.java index e901eecc..8fcac2ce 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/CollectionUtils.java +++ b/pgpainless-core/src/test/java/org/pgpainless/util/CollectionUtils.java @@ -4,6 +4,7 @@ package org.pgpainless.util; +import javax.annotation.Nonnull; import java.lang.reflect.Array; import java.util.ArrayList; import java.util.Collection; @@ -23,7 +24,8 @@ public final class CollectionUtils { * @param type * @return list */ - public static List iteratorToList(Iterator iterator) { + @Nonnull + public static List iteratorToList(@Nonnull Iterator iterator) { List items = new ArrayList<>(); while (iterator.hasNext()) { I item = iterator.next(); @@ -39,7 +41,8 @@ public final class CollectionUtils { * @param type * @return t and ts as array */ - public static T[] concat(T t, T[] ts) { + @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); @@ -53,7 +56,7 @@ public final class CollectionUtils { * @param type * @return true if ts contains t, false otherwise */ - public static boolean contains(T[] ts, T t) { + public static boolean contains(@Nonnull T[] ts, @Nonnull T t) { for (T i : ts) { if (i.equals(t)) { return true; @@ -69,7 +72,7 @@ public final class CollectionUtils { * @param iterator iterator to gather items from * @param collection collection to add items to */ - public static void addAll(Iterator iterator, Collection collection) { + 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/GuessPreferredHashAlgorithmTest.java b/pgpainless-core/src/test/java/org/pgpainless/util/GuessPreferredHashAlgorithmTest.java index ca7a06d1..e276ba8f 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/util/GuessPreferredHashAlgorithmTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/util/GuessPreferredHashAlgorithmTest.java @@ -21,7 +21,7 @@ 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 { @@ -30,7 +30,7 @@ public class GuessPreferredHashAlgorithmTest { public void guessPreferredHashAlgorithmsAssumesHashAlgoUsedBySelfSig() throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() - .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), + .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/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 bf4955cd..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,7 +52,7 @@ 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 @@ -73,16 +73,16 @@ public class KeyRingsFromCollectionTest { 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/userid/SelectUserIdTest.java b/pgpainless-core/src/test/java/org/pgpainless/util/selection/userid/SelectUserIdTest.java index 3a49247c..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,20 +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; -import static org.junit.jupiter.api.Assertions.assertTrue; - 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) @@ -34,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()) @@ -83,34 +87,32 @@ 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 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-sop/README.md b/pgpainless-sop/README.md index 3aef6d05..7f7267cd 100644 --- a/pgpainless-sop/README.md +++ b/pgpainless-sop/README.md @@ -6,7 +6,7 @@ SPDX-License-Identifier: Apache-2.0 # PGPainless-SOP -[![Spec Revision: 4](https://img.shields.io/badge/Spec%20Revision-4-blue)](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/) +[![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) @@ -23,7 +23,7 @@ To start using pgpainless-sop in your code, include the following lines in your ... dependencies { ... - implementation "org.pgpainless:pgpainless-sop:1.4.4" + implementation "org.pgpainless:pgpainless-sop:1.7.6" ... } @@ -34,7 +34,7 @@ dependencies { org.pgpainless pgpainless-sop - 1.4.4 + 1.7.6 ... @@ -67,7 +67,7 @@ byte[] encrypted = sop.encrypt() // Decrypt a message ByteArrayAndResult messageAndVerifications = sop.decrypt() - .verifyWith(cert) + .verifyWithCert(cert) .withKey(key) .ciphertext(encrypted) .toByteArrayAndResult(); diff --git a/pgpainless-sop/build.gradle b/pgpainless-sop/build.gradle index 1a40bb27..53b499c6 100644 --- a/pgpainless-sop/build.gradle +++ b/pgpainless-sop/build.gradle @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2021 Paul Schaub // // SPDX-License-Identifier: Apache-2.0 - +import org.apache.tools.ant.filters.* plugins { id 'java-library' } @@ -14,21 +14,36 @@ repositories { } dependencies { - implementation 'org.jetbrains:annotations:20.1.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" // 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(testFixtures("org.pgpainless:sop-java:$sopJavaVersion")) + testImplementation "org.pgpainless:sop-java-testfixtures:$sopJavaVersion" implementation(project(":pgpainless-core")) 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 daee3a9b..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/ArmorImpl.java +++ /dev/null @@ -1,58 +0,0 @@ -// SPDX-FileCopyrightText: 2020 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.bcpg.ArmoredOutputStream; -import org.bouncycastle.util.io.Streams; -import org.pgpainless.decryption_verification.OpenPgpInputStream; -import org.pgpainless.util.ArmoredOutputStreamFactory; -import sop.Ready; -import sop.enums.ArmorLabel; -import sop.exception.SOPGPException; -import sop.operation.Armor; - -/** - * Implementation of the
armor
operation using PGPainless. - */ -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 { - // By buffering the output stream, we can improve performance drastically - BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream); - - // Determine nature of the given data - OpenPgpInputStream openPgpIn = new OpenPgpInputStream(data); - openPgpIn.reset(); - - if (openPgpIn.isAsciiArmored()) { - // armoring already-armored data is an idempotent operation - Streams.pipeAll(openPgpIn, bufferedOutputStream); - bufferedOutputStream.flush(); - openPgpIn.close(); - return; - } - - ArmoredOutputStream armor = ArmoredOutputStreamFactory.get(bufferedOutputStream); - Streams.pipeAll(openPgpIn, armor); - bufferedOutputStream.flush(); - 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 29483437..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DearmorImpl.java +++ /dev/null @@ -1,42 +0,0 @@ -// SPDX-FileCopyrightText: 2021 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. - */ -public class DearmorImpl implements Dearmor { - - @Override - public Ready data(InputStream data) { - InputStream decoder; - try { - decoder = PGPUtil.getDecoderStream(data); - } catch (IOException e) { - throw new SOPGPException.BadData(e); - } - return new Ready() { - - @Override - public void writeTo(OutputStream outputStream) throws IOException { - BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream); - Streams.pipeAll(decoder, bufferedOutputStream); - bufferedOutputStream.flush(); - 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 f7876799..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java +++ /dev/null @@ -1,172 +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.nio.charset.Charset; -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.util.io.Streams; -import org.pgpainless.PGPainless; -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; -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.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.Verification; -import sop.exception.SOPGPException; -import sop.operation.Decrypt; - -/** - * Implementation of the
decrypt
operation using PGPainless. - */ -public class DecryptImpl implements Decrypt { - - private final ConsumerOptions consumerOptions = ConsumerOptions.get(); - private final MatchMakingSecretKeyRingProtector protector = new MatchMakingSecretKeyRingProtector(); - - @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 { - PGPPublicKeyRingCollection certs = KeyReader.readPublicKeys(certIn, true); - if (certs != null) { - consumerOptions.addVerificationCerts(certs); - } - return this; - } - - @Override - public DecryptImpl withSessionKey(SessionKey sessionKey) throws SOPGPException.UnsupportedOption { - consumerOptions.setSessionKey( - new org.pgpainless.util.SessionKey( - SymmetricKeyAlgorithm.requireFromId(sessionKey.getAlgorithm()), - sessionKey.getKey())); - return this; - } - - @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.BadData, IOException, SOPGPException.UnsupportedAsymmetricAlgo { - PGPSecretKeyRingCollection secretKeyCollection = KeyReader.readSecretKeys(keyIn, true); - - for (PGPSecretKeyRing key : secretKeyCollection) { - protector.addSecretKey(key); - consumerOptions.addDecryptionKey(key, protector); - } - return this; - } - - @Override - public Decrypt withKeyPassword(byte[] password) { - String string = new String(password, Charset.forName("UTF8")); - protector.addPassphrase(Passphrase.fromPassword(string)); - return this; - } - - @Override - public ReadyWithResult ciphertext(InputStream ciphertext) - throws SOPGPException.BadData, - SOPGPException.MissingArg { - - if (consumerOptions.getDecryptionKeys().isEmpty() && consumerOptions.getDecryptionPassphrases().isEmpty() && consumerOptions.getSessionKey() == null) { - throw new SOPGPException.MissingArg("Missing decryption key, passphrase or session key."); - } - - DecryptionStream decryptionStream; - try { - decryptionStream = PGPainless.decryptAndOrVerify() - .onInputStream(ciphertext) - .withOptions(consumerOptions); - } catch (MissingDecryptionMethodException e) { - throw new SOPGPException.CannotDecrypt("No usable decryption key or password provided.", e); - } catch (WrongPassphraseException e) { - throw new SOPGPException.KeyIsProtected(); - } catch (MalformedOpenPgpMessageException | PGPException | IOException e) { - throw new SOPGPException.BadData(e); - } finally { - // Forget passphrases after decryption - protector.clear(); - } - - return new ReadyWithResult() { - @Override - public DecryptionResult writeTo(OutputStream outputStream) throws IOException, SOPGPException.NoSignature { - Streams.pipeAll(decryptionStream, outputStream); - decryptionStream.close(); - MessageMetadata metadata = decryptionStream.getMetadata(); - - if (!metadata.isEncrypted()) { - throw new SOPGPException.BadData("Data is not encrypted."); - } - - List verificationList = new ArrayList<>(); - for (SignatureVerification signatureVerification : metadata.getVerifiedInlineSignatures()) { - verificationList.add(map(signatureVerification)); - } - - SessionKey sessionKey = null; - if (metadata.getSessionKey() != null) { - org.pgpainless.util.SessionKey sk = metadata.getSessionKey(); - sessionKey = new SessionKey( - (byte) sk.getAlgorithm().getAlgorithmId(), - sk.getKey() - ); - } - - return new DecryptionResult(sessionKey, verificationList); - } - }; - } - - private Verification map(SignatureVerification sigVerification) { - return new Verification(sigVerification.getSignature().getCreationTime(), - sigVerification.getSigningKey().getSubkeyFingerprint().toString(), - sigVerification.getSigningKey().getPrimaryKeyFingerprint().toString()); - } -} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedSignImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedSignImpl.java deleted file mode 100644 index c32cb219..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedSignImpl.java +++ /dev/null @@ -1,163 +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.nio.charset.Charset; -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.exception.KeyException; -import org.pgpainless.key.OpenPgpFingerprint; -import org.pgpainless.key.SubkeyIdentifier; -import org.pgpainless.key.info.KeyRingInfo; -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; - -/** - * Implementation of the
sign
operation using PGPainless. - */ -public class DetachedSignImpl implements DetachedSign { - - private boolean armor = true; - private SignAs mode = SignAs.Binary; - private final SigningOptions signingOptions = SigningOptions.get(); - private final MatchMakingSecretKeyRingProtector protector = new MatchMakingSecretKeyRingProtector(); - private final List signingKeys = new ArrayList<>(); - - @Override - public DetachedSign noArmor() { - armor = false; - return this; - } - - @Override - public DetachedSign mode(SignAs mode) { - this.mode = mode; - return this; - } - - @Override - public DetachedSign key(InputStream keyIn) throws SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException { - PGPSecretKeyRingCollection keys = KeyReader.readSecretKeys(keyIn, true); - for (PGPSecretKeyRing key : keys) { - KeyRingInfo info = PGPainless.inspectKeyRing(key); - if (!info.isUsableForSigning()) { - throw new SOPGPException.KeyCannotSign("Key " + info.getFingerprint() + " does not have valid, signing capable subkeys."); - } - protector.addSecretKey(key); - signingKeys.add(key); - } - return this; - } - - @Override - public DetachedSign withKeyPassword(byte[] password) { - String string = new String(password, Charset.forName("UTF8")); - protector.addPassphrase(Passphrase.fromPassword(string)); - return this; - } - - @Override - public ReadyWithResult data(InputStream data) throws IOException { - for (PGPSecretKeyRing key : signingKeys) { - try { - signingOptions.addDetachedSignature(protector, key, modeToSigType(mode)); - } catch (KeyException.UnacceptableSigningKeyException | KeyException.MissingSecretKeyException e) { - throw new SOPGPException.KeyCannotSign("Key " + OpenPgpFingerprint.of(key) + " cannot sign.", e); - } catch (PGPException e) { - throw new SOPGPException.KeyIsProtected("Key " + OpenPgpFingerprint.of(key) + " cannot be unlocked.", e); - } - } - - ByteArrayOutputStream buffer = new ByteArrayOutputStream(); - try { - EncryptionStream signingStream = PGPainless.encryptAndOrSign() - .onOutputStream(buffer) - .withOptions(ProducerOptions.sign(signingOptions) - .setAsciiArmor(armor)); - - return new ReadyWithResult() { - @Override - public SigningResult 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(); - - // forget passphrases - protector.clear(); - - 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 - - return SigningResult.builder() - .setMicAlg(micAlgFromSignatures(signatures)) - .build(); - } - }; - - } catch (PGPException e) { - throw new RuntimeException(e); - } - - } - - private MicAlg micAlgFromSignatures(Iterable signatures) { - int algorithmId = 0; - for (PGPSignature signature : signatures) { - int sigAlg = signature.getHashAlgorithm(); - if (algorithmId == 0 || algorithmId == sigAlg) { - algorithmId = sigAlg; - } else { - return MicAlg.empty(); - } - } - return algorithmId == 0 ? MicAlg.empty() : MicAlg.fromHashAlgorithmId(algorithmId); - } - - 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/DetachedVerifyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java deleted file mode 100644 index 93ad398c..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java +++ /dev/null @@ -1,99 +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.util.io.Streams; -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.decryption_verification.SignatureVerification; -import org.pgpainless.exception.MalformedOpenPgpMessageException; -import sop.Verification; -import sop.exception.SOPGPException; -import sop.operation.DetachedVerify; - -/** - * Implementation of the
verify
operation using PGPainless. - */ -public class DetachedVerifyImpl implements DetachedVerify { - - private final ConsumerOptions options = ConsumerOptions.get(); - - @Override - public DetachedVerify notBefore(Date timestamp) throws SOPGPException.UnsupportedOption { - options.verifyNotBefore(timestamp); - return this; - } - - @Override - public DetachedVerify notAfter(Date timestamp) throws SOPGPException.UnsupportedOption { - options.verifyNotAfter(timestamp); - return this; - } - - @Override - public DetachedVerify cert(InputStream cert) throws SOPGPException.BadData, IOException { - PGPPublicKeyRingCollection certificates = KeyReader.readPublicKeys(cert, true); - options.addVerificationCerts(certificates); - return this; - } - - @Override - public DetachedVerifyImpl 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 { - options.forceNonOpenPgpData(); - - DecryptionStream decryptionStream; - try { - decryptionStream = PGPainless.decryptAndOrVerify() - .onInputStream(data) - .withOptions(options); - - Streams.drain(decryptionStream); - decryptionStream.close(); - - MessageMetadata metadata = decryptionStream.getMetadata(); - List verificationList = new ArrayList<>(); - - for (SignatureVerification signatureVerification : metadata.getVerifiedDetachedSignatures()) { - verificationList.add(map(signatureVerification)); - } - - if (!options.getCertificateSource().getExplicitCertificates().isEmpty()) { - if (verificationList.isEmpty()) { - throw new SOPGPException.NoSignature(); - } - } - - return verificationList; - } catch (MalformedOpenPgpMessageException | PGPException e) { - throw new SOPGPException.BadData(e); - } - } - - private Verification map(SignatureVerification sigVerification) { - return new Verification(sigVerification.getSignature().getCreationTime(), - sigVerification.getSigningKey().getSubkeyFingerprint().toString(), - sigVerification.getSigningKey().getPrimaryKeyFingerprint().toString()); - } -} 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 62d20bf0..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java +++ /dev/null @@ -1,169 +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.nio.charset.Charset; -import java.util.HashSet; -import java.util.Set; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -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.KeyException; -import org.pgpainless.exception.WrongPassphraseException; -import org.pgpainless.key.OpenPgpFingerprint; -import org.pgpainless.key.info.KeyRingInfo; -import org.pgpainless.util.Passphrase; -import sop.Ready; -import sop.enums.EncryptAs; -import sop.exception.SOPGPException; -import sop.operation.Encrypt; -import sop.util.ProxyOutputStream; - -/** - * Implementation of the
encrypt
operation using PGPainless. - */ -public class EncryptImpl implements Encrypt { - - EncryptionOptions encryptionOptions = EncryptionOptions.get(); - SigningOptions signingOptions = null; - MatchMakingSecretKeyRingProtector protector = new MatchMakingSecretKeyRingProtector(); - private final Set signingKeys = new HashSet<>(); - - 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.KeyCannotSign, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData, IOException { - if (signingOptions == null) { - signingOptions = SigningOptions.get(); - } - PGPSecretKeyRingCollection keys = KeyReader.readSecretKeys(keyIn, true); - if (keys.size() != 1) { - throw new SOPGPException.BadData(new AssertionError("Exactly one secret key at a time expected. Got " + keys.size())); - } - PGPSecretKeyRing signingKey = keys.iterator().next(); - - KeyRingInfo info = PGPainless.inspectKeyRing(signingKey); - if (info.getSigningSubkeys().isEmpty()) { - throw new SOPGPException.KeyCannotSign("Key " + OpenPgpFingerprint.of(signingKey) + " cannot sign."); - } - - protector.addSecretKey(signingKey); - signingKeys.add(signingKey); - return this; - } - - @Override - public Encrypt withKeyPassword(byte[] password) { - String passphrase = new String(password, Charset.forName("UTF8")); - protector.addPassphrase(Passphrase.fromPassword(passphrase)); - 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() - .publicKeyRingCollection(cert); - if (certificates.size() == 0) { - throw new SOPGPException.BadData("No certificate data found."); - } - encryptionOptions.addRecipients(certificates); - } catch (KeyException.UnacceptableEncryptionKeyException e) { - throw new SOPGPException.CertCannotEncrypt(e.getMessage(), e); - } catch (IOException | PGPException e) { - throw new SOPGPException.BadData(e); - } - return this; - } - - @Override - public Ready plaintext(InputStream plaintext) throws IOException { - if (!encryptionOptions.hasEncryptionMethod()) { - throw new SOPGPException.MissingArg("Missing encryption method."); - } - ProducerOptions producerOptions = signingOptions != null ? - ProducerOptions.signAndEncrypt(encryptionOptions, signingOptions) : - ProducerOptions.encrypt(encryptionOptions); - producerOptions.setAsciiArmor(armor); - producerOptions.setEncoding(encryptAsToStreamEncoding(encryptAs)); - - for (PGPSecretKeyRing signingKey : signingKeys) { - try { - signingOptions.addInlineSignature( - protector, - signingKey, - (encryptAs == EncryptAs.Binary ? DocumentSignatureType.BINARY_DOCUMENT : DocumentSignatureType.CANONICAL_TEXT_DOCUMENT) - ); - } catch (KeyException.UnacceptableSigningKeyException e) { - throw new SOPGPException.KeyCannotSign(); - } catch (WrongPassphraseException e) { - throw new SOPGPException.KeyIsProtected(); - } catch (PGPException e) { - throw new SOPGPException.BadData(e); - } - } - - 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.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 be7fc9c3..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/ExtractCertImpl.java +++ /dev/null @@ -1,60 +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.List; - -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; -import org.pgpainless.PGPainless; -import org.pgpainless.util.ArmorUtils; -import sop.Ready; -import sop.exception.SOPGPException; -import sop.operation.ExtractCert; - -/** - * Implementation of the
extract-cert
operation using PGPainless. - */ -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 { - PGPSecretKeyRingCollection keys = KeyReader.readSecretKeys(keyInputStream, true); - - List certs = new ArrayList<>(); - for (PGPSecretKeyRing key : keys) { - PGPPublicKeyRing cert = PGPainless.extractCertificate(key); - certs.add(cert); - } - - return new Ready() { - @Override - public void writeTo(OutputStream outputStream) throws IOException { - - for (PGPPublicKeyRing cert : certs) { - OutputStream out = armor ? ArmorUtils.toAsciiArmoredStream(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 da99c854..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java +++ /dev/null @@ -1,93 +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 org.pgpainless.util.Passphrase; -import sop.Ready; -import sop.exception.SOPGPException; -import sop.operation.GenerateKey; - -/** - * Implementation of the
generate-key
operation using PGPainless. - */ -public class GenerateKeyImpl implements GenerateKey { - - private boolean armor = true; - private final Set userIds = new LinkedHashSet<>(); - private Passphrase passphrase = Passphrase.emptyPassphrase(); - - @Override - public GenerateKey noArmor() { - this.armor = false; - return this; - } - - @Override - public GenerateKey userId(String userId) { - this.userIds.add(userId); - return this; - } - - @Override - public GenerateKey withKeyPassword(String password) { - this.passphrase = Passphrase.fromPassword(password); - return this; - } - - @Override - public Ready generate() throws SOPGPException.MissingArg, SOPGPException.UnsupportedAsymmetricAlgo { - Iterator userIdIterator = userIds.iterator(); - Passphrase passphraseCopy = new Passphrase(passphrase.getChars()); // generateKeyRing clears the original passphrase - PGPSecretKeyRing key; - try { - String primaryUserId = userIdIterator.hasNext() ? userIdIterator.next() : null; - key = PGPainless.generateKeyRing() - .modernKeyRing(primaryUserId, passphrase); - - if (userIdIterator.hasNext()) { - SecretKeyRingEditorInterface editor = PGPainless.modifyKeyRing(key); - - while (userIdIterator.hasNext()) { - editor.addUserId(userIdIterator.next(), SecretKeyRingProtector.unlockAnyKeyWith(passphraseCopy)); - } - - 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/InlineDetachImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineDetachImpl.java deleted file mode 100644 index bafc2794..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineDetachImpl.java +++ /dev/null @@ -1,152 +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.ArmoredInputStream; -import org.bouncycastle.bcpg.ArmoredOutputStream; -import org.bouncycastle.openpgp.PGPCompressedData; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPLiteralData; -import org.bouncycastle.openpgp.PGPObjectFactory; -import org.bouncycastle.openpgp.PGPOnePassSignatureList; -import org.bouncycastle.openpgp.PGPSignature; -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. - */ -public class InlineDetachImpl implements InlineDetach { - - private boolean armor = true; - - @Override - public InlineDetach noArmor() { - this.armor = false; - return this; - } - - @Override - public ReadyWithResult message(InputStream messageInputStream) { - - return new ReadyWithResult() { - - private final ByteArrayOutputStream sigOut = new ByteArrayOutputStream(); - - @Override - public Signatures writeTo(OutputStream messageOutputStream) - throws SOPGPException.NoSignature, IOException { - - PGPSignatureList signatures = null; - OpenPgpInputStream pgpIn = new OpenPgpInputStream(messageInputStream); - - if (pgpIn.isNonOpenPgp()) { - throw new SOPGPException.BadData("Data appears to be non-OpenPGP."); - } - - // handle ASCII armor - if (pgpIn.isAsciiArmored()) { - ArmoredInputStream armorIn = new ArmoredInputStream(pgpIn); - - // Handle cleartext signature framework - if (armorIn.isClearText()) { - try { - signatures = ClearsignedMessageUtil.detachSignaturesFromInbandClearsignedMessage(armorIn, messageOutputStream); - if (signatures == null) { - throw new SOPGPException.BadData("Data did not contain OpenPGP signatures."); - } - } catch (WrongConsumingMethodException e) { - throw new SOPGPException.BadData(e); - } - } - // else just dearmor - pgpIn = new OpenPgpInputStream(armorIn); - } - - // if data was not using cleartext signatures framework - if (signatures == null) { - - if (!pgpIn.isBinaryOpenPgp()) { - throw new SOPGPException.BadData("Data was containing ASCII armored non-OpenPGP data."); - } - - // handle binary OpenPGP data - PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(pgpIn); - Object next; - while ((next = objectFactory.nextObject()) != null) { - - if (next instanceof PGPOnePassSignatureList) { - // skip over ops - continue; - } - - if (next instanceof PGPLiteralData) { - // write out contents of literal data packet - PGPLiteralData literalData = (PGPLiteralData) next; - InputStream literalIn = literalData.getDataStream(); - Streams.pipeAll(literalIn, messageOutputStream); - literalIn.close(); - continue; - } - - if (next instanceof PGPCompressedData) { - // decompress compressed data - PGPCompressedData compressedData = (PGPCompressedData) next; - try { - objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(compressedData.getDataStream()); - } catch (PGPException e) { - throw new SOPGPException.BadData("Cannot decompress PGPCompressedData", e); - } - continue; - } - - if (next instanceof PGPSignatureList) { - signatures = (PGPSignatureList) next; - } - } - } - - if (signatures == null) { - throw new SOPGPException.BadData("Data did not contain OpenPGP signatures."); - } - - // write out signatures - 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/InlineSignImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java deleted file mode 100644 index dd4ab0cf..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java +++ /dev/null @@ -1,128 +0,0 @@ -// SPDX-FileCopyrightText: 2022 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.nio.charset.Charset; -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.util.io.Streams; -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.exception.KeyException; -import org.pgpainless.key.OpenPgpFingerprint; -import org.pgpainless.key.info.KeyRingInfo; -import org.pgpainless.util.Passphrase; -import sop.Ready; -import sop.enums.InlineSignAs; -import sop.exception.SOPGPException; -import sop.operation.InlineSign; - -/** - * Implementation of the
inline-sign
operation using PGPainless. - */ -public class InlineSignImpl implements InlineSign { - - private boolean armor = true; - private InlineSignAs mode = InlineSignAs.binary; - private final SigningOptions signingOptions = new SigningOptions(); - private final MatchMakingSecretKeyRingProtector protector = new MatchMakingSecretKeyRingProtector(); - private final List signingKeys = new ArrayList<>(); - - @Override - public InlineSign mode(InlineSignAs mode) throws SOPGPException.UnsupportedOption { - this.mode = mode; - return this; - } - - @Override - public InlineSign noArmor() { - this.armor = false; - return this; - } - - @Override - public InlineSign key(InputStream keyIn) throws SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException { - PGPSecretKeyRingCollection keys = KeyReader.readSecretKeys(keyIn, true); - for (PGPSecretKeyRing key : keys) { - KeyRingInfo info = PGPainless.inspectKeyRing(key); - if (!info.isUsableForSigning()) { - throw new SOPGPException.KeyCannotSign("Key " + info.getFingerprint() + " does not have valid, signing capable subkeys."); - } - protector.addSecretKey(key); - signingKeys.add(key); - } - return this; - } - - @Override - public InlineSign withKeyPassword(byte[] password) { - String string = new String(password, Charset.forName("UTF8")); - protector.addPassphrase(Passphrase.fromPassword(string)); - return this; - } - - @Override - public Ready data(InputStream data) throws SOPGPException.KeyIsProtected, SOPGPException.ExpectedText { - for (PGPSecretKeyRing key : signingKeys) { - try { - if (mode == InlineSignAs.clearsigned) { - signingOptions.addDetachedSignature(protector, key, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT); - } else { - signingOptions.addInlineSignature(protector, key, modeToSigType(mode)); - } - } catch (KeyException.UnacceptableSigningKeyException | KeyException.MissingSecretKeyException e) { - throw new SOPGPException.KeyCannotSign("Key " + OpenPgpFingerprint.of(key) + " cannot sign.", e); - } catch (PGPException e) { - throw new SOPGPException.KeyIsProtected("Key " + OpenPgpFingerprint.of(key) + " cannot be unlocked.", e); - } - } - - ProducerOptions producerOptions = ProducerOptions.sign(signingOptions); - if (mode == InlineSignAs.clearsigned) { - producerOptions.setCleartextSigned(); - producerOptions.setAsciiArmor(true); - } else { - producerOptions.setAsciiArmor(armor); - } - - return new Ready() { - @Override - public void writeTo(OutputStream outputStream) throws IOException, SOPGPException.NoSignature { - try { - EncryptionStream signingStream = PGPainless.encryptAndOrSign() - .onOutputStream(outputStream) - .withOptions(producerOptions); - - if (signingStream.isClosed()) { - throw new IllegalStateException("EncryptionStream is already closed."); - } - - Streams.pipeAll(data, signingStream); - signingStream.close(); - - // forget passphrases - protector.clear(); - } catch (PGPException e) { - throw new RuntimeException(e); - } - } - }; - } - - private static DocumentSignatureType modeToSigType(InlineSignAs mode) { - return mode == InlineSignAs.binary ? DocumentSignatureType.BINARY_DOCUMENT - : DocumentSignatureType.CANONICAL_TEXT_DOCUMENT; - } -} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java deleted file mode 100644 index 7665a7bb..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java +++ /dev/null @@ -1,101 +0,0 @@ -// SPDX-FileCopyrightText: 2022 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.util.io.Streams; -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.decryption_verification.SignatureVerification; -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. - */ -public class InlineVerifyImpl implements InlineVerify { - - private final ConsumerOptions options = ConsumerOptions.get(); - - @Override - public InlineVerify notBefore(Date timestamp) throws SOPGPException.UnsupportedOption { - options.verifyNotBefore(timestamp); - return this; - } - - @Override - public InlineVerify notAfter(Date timestamp) throws SOPGPException.UnsupportedOption { - options.verifyNotAfter(timestamp); - return this; - } - - @Override - public InlineVerify cert(InputStream cert) throws SOPGPException.BadData, IOException { - PGPPublicKeyRingCollection certificates = KeyReader.readPublicKeys(cert, true); - options.addVerificationCerts(certificates); - return this; - } - - @Override - public ReadyWithResult> data(InputStream data) throws SOPGPException.NoSignature, SOPGPException.BadData { - return new ReadyWithResult>() { - @Override - public List writeTo(OutputStream outputStream) throws IOException, SOPGPException.NoSignature { - DecryptionStream decryptionStream; - try { - decryptionStream = PGPainless.decryptAndOrVerify() - .onInputStream(data) - .withOptions(options); - - Streams.pipeAll(decryptionStream, outputStream); - decryptionStream.close(); - - MessageMetadata metadata = decryptionStream.getMetadata(); - List verificationList = new ArrayList<>(); - - List verifications = metadata.isUsingCleartextSignatureFramework() ? - metadata.getVerifiedDetachedSignatures() : - metadata.getVerifiedInlineSignatures(); - - for (SignatureVerification signatureVerification : verifications) { - verificationList.add(map(signatureVerification)); - } - - if (!options.getCertificateSource().getExplicitCertificates().isEmpty()) { - if (verificationList.isEmpty()) { - throw new SOPGPException.NoSignature(); - } - } - - return verificationList; - } catch (MissingDecryptionMethodException e) { - throw new SOPGPException.BadData("Cannot verify encrypted message.", e); - } catch (MalformedOpenPgpMessageException | PGPException e) { - throw new SOPGPException.BadData(e); - } - } - }; - } - - private Verification map(SignatureVerification sigVerification) { - return new Verification(sigVerification.getSignature().getCreationTime(), - sigVerification.getSigningKey().getSubkeyFingerprint().toString(), - sigVerification.getSigningKey().getPrimaryKeyFingerprint().toString()); - } -} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/KeyReader.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/KeyReader.java deleted file mode 100644 index 036ec126..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/KeyReader.java +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.sop; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; -import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; -import org.pgpainless.PGPainless; -import sop.exception.SOPGPException; - -import java.io.IOException; -import java.io.InputStream; - -/** - * Reader for OpenPGP keys and certificates with error matching according to the SOP spec. - */ -class KeyReader { - - static PGPSecretKeyRingCollection readSecretKeys(InputStream keyInputStream, boolean requireContent) - throws IOException, SOPGPException.BadData { - PGPSecretKeyRingCollection keys; - try { - keys = PGPainless.readKeyRing().secretKeyRingCollection(keyInputStream); - } catch (IOException e) { - String message = e.getMessage(); - if (message == null) { - throw e; - } - if (message.startsWith("unknown object in stream:") || - message.startsWith("invalid header encountered")) { - throw new SOPGPException.BadData(e); - } - throw e; - } catch (PGPException e) { - throw new SOPGPException.BadData("Cannot read keys.", e); - } - - if (requireContent && (keys == null || keys.size() == 0)) { - throw new SOPGPException.BadData(new PGPException("No key data found.")); - } - - return keys; - } - - static PGPPublicKeyRingCollection readPublicKeys(InputStream certIn, boolean requireContent) - throws IOException { - PGPPublicKeyRingCollection certs; - try { - certs = PGPainless.readKeyRing().publicKeyRingCollection(certIn); - } catch (IOException e) { - if (e.getMessage() != null && e.getMessage().startsWith("unknown object in stream:")) { - throw new SOPGPException.BadData(e); - } - throw e; - } catch (PGPException e) { - throw new SOPGPException.BadData(e); - } - if (requireContent && (certs == null || certs.size() == 0)) { - throw new SOPGPException.BadData(new PGPException("No cert data found.")); - } - return certs; - } -} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/MatchMakingSecretKeyRingProtector.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/MatchMakingSecretKeyRingProtector.java deleted file mode 100644 index 0b88cb5d..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/MatchMakingSecretKeyRingProtector.java +++ /dev/null @@ -1,118 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.sop; - -import java.util.HashSet; -import java.util.Set; - -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.jetbrains.annotations.Nullable; -import org.pgpainless.implementation.ImplementationFactory; -import org.pgpainless.key.info.KeyInfo; -import org.pgpainless.key.protection.CachingSecretKeyRingProtector; -import org.pgpainless.key.protection.SecretKeyRingProtector; -import org.pgpainless.key.protection.UnlockSecretKey; -import org.pgpainless.util.Passphrase; - -/** - * Implementation of the {@link SecretKeyRingProtector} which can be handed passphrases and keys separately, - * and which then matches up passphrases and keys when needed. - */ -public class MatchMakingSecretKeyRingProtector implements SecretKeyRingProtector { - - private final Set passphrases = new HashSet<>(); - private final Set keys = new HashSet<>(); - private final CachingSecretKeyRingProtector protector = new CachingSecretKeyRingProtector(); - - /** - * Add a single passphrase to the protector. - * - * @param passphrase passphrase - */ - public void addPassphrase(Passphrase passphrase) { - if (passphrase.isEmpty()) { - return; - } - - if (!passphrases.add(passphrase)) { - return; - } - - for (PGPSecretKeyRing key : keys) { - for (PGPSecretKey subkey : key) { - if (protector.hasPassphrase(subkey.getKeyID())) { - continue; - } - - testPassphrase(passphrase, subkey); - } - } - } - - /** - * Add a single {@link PGPSecretKeyRing} to the protector. - * - * @param key secret keys - */ - public void addSecretKey(PGPSecretKeyRing key) { - if (!keys.add(key)) { - return; - } - - for (PGPSecretKey subkey : key) { - if (KeyInfo.isDecrypted(subkey)) { - protector.addPassphrase(subkey.getKeyID(), Passphrase.emptyPassphrase()); - } else { - for (Passphrase passphrase : passphrases) { - testPassphrase(passphrase, subkey); - } - } - } - } - - private void testPassphrase(Passphrase passphrase, PGPSecretKey subkey) { - try { - PBESecretKeyDecryptor decryptor = ImplementationFactory.getInstance().getPBESecretKeyDecryptor(passphrase); - UnlockSecretKey.unlockSecretKey(subkey, decryptor); - protector.addPassphrase(subkey.getKeyID(), passphrase); - } catch (PGPException e) { - // wrong password - } - } - - @Override - public boolean hasPassphraseFor(Long keyId) { - return protector.hasPassphrase(keyId); - } - - @Nullable - @Override - public PBESecretKeyDecryptor getDecryptor(Long keyId) throws PGPException { - return protector.getDecryptor(keyId); - } - - @Nullable - @Override - public PBESecretKeyEncryptor getEncryptor(Long keyId) throws PGPException { - return protector.getEncryptor(keyId); - } - - /** - * Clear all known passphrases from the protector. - */ - public void clear() { - for (Passphrase passphrase : passphrases) { - passphrase.clear(); - } - - for (PGPSecretKeyRing key : keys) { - protector.forgetPassphrase(key); - } - } -} 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 a49f7e34..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java +++ /dev/null @@ -1,103 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.sop; - -import org.pgpainless.util.ArmoredOutputStreamFactory; -import sop.SOP; -import sop.operation.Armor; -import sop.operation.Dearmor; -import sop.operation.Decrypt; -import sop.operation.DetachedSign; -import sop.operation.DetachedVerify; -import sop.operation.InlineDetach; -import sop.operation.Encrypt; -import sop.operation.ExtractCert; -import sop.operation.GenerateKey; -import sop.operation.InlineSign; -import sop.operation.InlineVerify; -import sop.operation.Version; - -/** - * Implementation of the
sop
API using PGPainless. - *
 {@code
- * SOP sop = new SOPImpl();
- * }
- */ -public class SOPImpl implements SOP { - - static { - ArmoredOutputStreamFactory.setVersionInfo(null); - } - - @Override - public Version version() { - return new VersionImpl(); - } - - @Override - public GenerateKey generateKey() { - return new GenerateKeyImpl(); - } - - @Override - public ExtractCert extractCert() { - return new ExtractCertImpl(); - } - - @Override - public DetachedSign sign() { - return detachedSign(); - } - - @Override - public DetachedSign detachedSign() { - return new DetachedSignImpl(); - } - - @Override - public InlineSign inlineSign() { - return new InlineSignImpl(); - } - - @Override - public DetachedVerify verify() { - return detachedVerify(); - } - - @Override - public DetachedVerify detachedVerify() { - return new DetachedVerifyImpl(); - } - - @Override - public InlineVerify inlineVerify() { - return new InlineVerifyImpl(); - } - - @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 InlineDetach inlineDetach() { - return new InlineDetachImpl(); - } -} 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 4449af10..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java +++ /dev/null @@ -1,66 +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.Locale; -import java.util.Properties; - -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import sop.operation.Version; - -/** - * Implementation of the
version
operation using PGPainless. - */ -public class VersionImpl implements Version { - - // draft version - private static final String SOP_VERSION = "04"; - - @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; - } - - @Override - public String getBackendVersion() { - double bcVersion = new BouncyCastleProvider().getVersion(); - return String.format(Locale.US, "Bouncy Castle %.2f", bcVersion); - } - - @Override - public String getExtendedVersion() { - return getName() + " " + getVersion() + "\n" + - "https://codeberg.org/PGPainless/pgpainless/src/branch/master/pgpainless-sop\n" + - "\n" + - "Implementation of the Stateless OpenPGP Protocol Version " + SOP_VERSION + "\n" + - "https://datatracker.ietf.org/doc/html/draft-dkg-openpgp-stateless-cli-" + SOP_VERSION + "\n" + - "\n" + - "Based on pgpainless-core " + getVersion() + "\n" + - "https://pgpainless.org\n" + - "\n" + - "Using " + getBackendVersion() + "\n" + - "https://www.bouncycastle.org/java.html"; - } -} 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 3830912a..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,30 +4,19 @@ 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 { + 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 diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/CarolKeySignEncryptRoundtripTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/CarolKeySignEncryptRoundtripTest.java index 81bf7645..83778106 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/CarolKeySignEncryptRoundtripTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/CarolKeySignEncryptRoundtripTest.java @@ -4,15 +4,16 @@ 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.Ready; - -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; +import sop.EncryptionResult; +import sop.ReadyWithResult; +import sop.testsuite.assertions.VerificationListAssert; public class CarolKeySignEncryptRoundtripTest { @@ -276,11 +277,11 @@ public class CarolKeySignEncryptRoundtripTest { public void regressionTest() throws IOException { SOPImpl sop = new SOPImpl(); byte[] msg = "Hello, World!\n".getBytes(); - Ready encryption = sop.encrypt() + ReadyWithResult encryption = sop.encrypt() .signWith(CAROL_KEY.getBytes()) .withCert(BOB_CERT.getBytes()) .plaintext(msg); - byte[] ciphertext = encryption.getBytes(); + byte[] ciphertext = encryption.toByteArrayAndResult().getBytes(); ByteArrayAndResult decryption = sop.decrypt() .withKey(BOB_KEY.getBytes()) @@ -290,6 +291,8 @@ public class CarolKeySignEncryptRoundtripTest { byte[] plaintext = decryption.getBytes(); assertArrayEquals(msg, plaintext); - assertEquals(1, decryption.getResult().getVerifications().size()); + VerificationListAssert.assertThatVerificationList(decryption.getResult().getVerifications()) + .hasSingleItem() + .issuedBy("71FFDA004409E5DDB0C3E8F19BA789DC76D6849A", "71FFDA004409E5DDB0C3E8F19BA789DC76D6849A"); } } diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/DetachedSignTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/DetachedSignTest.java index c6fcc267..7b2c9131 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/DetachedSignTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/DetachedSignTest.java @@ -23,7 +23,9 @@ 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 DetachedSignTest { @@ -49,6 +51,7 @@ public class DetachedSignTest { public void signArmored() throws IOException { byte[] signature = sop.sign() .key(key) + .mode(SignAs.binary) .data(data) .toByteArrayAndResult().getBytes(); @@ -56,12 +59,14 @@ public class DetachedSignTest { 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,12 +81,34 @@ public class DetachedSignTest { 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 @@ -93,7 +120,7 @@ public class DetachedSignTest { 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)); } @@ -107,7 +134,7 @@ public class DetachedSignTest { 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)); } @@ -115,7 +142,7 @@ public class DetachedSignTest { @Test public void mode() throws IOException, PGPException { byte[] signature = sop.sign() - .mode(SignAs.Text) + .mode(SignAs.text) .key(key) .data(data) .toByteArrayAndResult().getBytes(); 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 22af5b70..aa366ef1 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/EncryptDecryptRoundTripTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/EncryptDecryptRoundTripTest.java @@ -21,7 +21,9 @@ 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 { @@ -62,6 +64,7 @@ public class EncryptDecryptRoundTripTest { .withCert(aliceCert) .withCert(bobCert) .plaintext(message) + .toByteArrayAndResult() .getBytes(); ByteArrayAndResult bytesAndResult = sop.decrypt() @@ -75,7 +78,8 @@ public class EncryptDecryptRoundTripTest { assertArrayEquals(message, decrypted.toByteArray()); DecryptionResult result = bytesAndResult.getResult(); - assertEquals(1, result.getVerifications().size()); + VerificationListAssert.assertThatVerificationList(result.getVerifications()) + .hasSingleItem(); } @Test @@ -94,6 +98,7 @@ public class EncryptDecryptRoundTripTest { .withCert(aliceCertNoArmor) .noArmor() .plaintext(message) + .toByteArrayAndResult() .getBytes(); ByteArrayAndResult bytesAndResult = sop.decrypt() @@ -106,7 +111,8 @@ public class EncryptDecryptRoundTripTest { assertArrayEquals(message, decrypted); DecryptionResult result = bytesAndResult.getResult(); - assertEquals(1, result.getVerifications().size()); + VerificationListAssert.assertThatVerificationList(result.getVerifications()) + .hasSingleItem(); } @Test @@ -114,6 +120,7 @@ public class EncryptDecryptRoundTripTest { byte[] encrypted = sop.encrypt() .withPassword("passphr4s3") .plaintext(message) + .toByteArrayAndResult() .getBytes(); ByteArrayAndResult bytesAndResult = sop.decrypt() @@ -125,7 +132,8 @@ public class EncryptDecryptRoundTripTest { assertArrayEquals(message, decrypted); DecryptionResult result = bytesAndResult.getResult(); - assertEquals(0, result.getVerifications().size()); + VerificationListAssert.assertThatVerificationList(result.getVerifications()) + .isEmpty(); } @Test @@ -136,6 +144,7 @@ public class EncryptDecryptRoundTripTest { sop.encrypt() .withPassword("passphr4s3") .plaintext(message) + .toByteArrayAndResult() .getInputStream() ) .toByteArrayAndResult(); @@ -144,7 +153,8 @@ public class EncryptDecryptRoundTripTest { assertArrayEquals(message, decrypted); DecryptionResult result = bytesAndResult.getResult(); - assertEquals(0, result.getVerifications().size()); + VerificationListAssert.assertThatVerificationList(result.getVerifications()) + .isEmpty(); } @Test @@ -152,6 +162,7 @@ public class EncryptDecryptRoundTripTest { byte[] encrypted = sop.encrypt() .withPassword("passphr4s3 ") .plaintext(message) + .toByteArrayAndResult() .getBytes(); ByteArrayAndResult bytesAndResult = sop.decrypt() @@ -163,7 +174,8 @@ public class EncryptDecryptRoundTripTest { assertArrayEquals(message, decrypted); DecryptionResult result = bytesAndResult.getResult(); - assertEquals(0, result.getVerifications().size()); + VerificationListAssert.assertThatVerificationList(result.getVerifications()) + .isEmpty(); } @Test @@ -171,6 +183,7 @@ public class EncryptDecryptRoundTripTest { byte[] encrypted = sop.encrypt() .withCert(bobCert) .plaintext(message) + .toByteArrayAndResult() .getBytes(); DecryptionResult result = sop.decrypt() @@ -180,7 +193,8 @@ public class EncryptDecryptRoundTripTest { .toByteArrayAndResult() .getResult(); - assertTrue(result.getVerifications().isEmpty()); + VerificationListAssert.assertThatVerificationList(result.getVerifications()) + .isEmpty(); } @Test @@ -188,6 +202,7 @@ public class EncryptDecryptRoundTripTest { byte[] encrypted = sop.encrypt() .withCert(bobCert) .plaintext(message) + .toByteArrayAndResult() .getBytes(); assertThrows(SOPGPException.MissingArg.class, () -> sop @@ -258,6 +273,7 @@ public class EncryptDecryptRoundTripTest { byte[] ciphertext = sop.encrypt() .withCert(cert) .plaintext(plaintext) + .toByteArrayAndResult() .getBytes(); byte[] decrypted = sop.decrypt() @@ -300,6 +316,7 @@ public class EncryptDecryptRoundTripTest { .withCert(cert1) .withCert(cert2) .plaintext(plaintext) + .toByteArrayAndResult() .getBytes(); byte[] decrypted = sop.decrypt() @@ -331,6 +348,7 @@ public class EncryptDecryptRoundTripTest { byte[] ciphertext = sop.encrypt() .withCert(cert) .plaintext(plaintext) + .toByteArrayAndResult() .getBytes(); assertThrows(SOPGPException.KeyIsProtected.class, @@ -486,14 +504,19 @@ public class EncryptDecryptRoundTripTest { sop.decrypt().withKey(key).verifyWithCert(cert).ciphertext(ciphertext).toByteArrayAndResult(); assertEquals(sessionKey, bytesAndResult.getResult().getSessionKey().get().toString()); assertArrayEquals(plaintext, bytesAndResult.getBytes()); - assertEquals(1, bytesAndResult.getResult().getVerifications().size()); - + 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()); - assertEquals(1, bytesAndResult.getResult().getVerifications().size()); + VerificationListAssert.assertThatVerificationList(bytesAndResult.getResult().getVerifications()) + .hasSingleItem() + .issuedBy("9C26EFAB1C6500A228E8A9C2658EE420C824D191") + .hasMode(SignatureMode.binary); } @Test @@ -533,4 +556,44 @@ public class EncryptDecryptRoundTripTest { .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/GenerateKeyTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/GenerateKeyTest.java index 3a6e4476..521cdfe0 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/GenerateKeyTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/GenerateKeyTest.java @@ -7,6 +7,7 @@ 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; @@ -21,6 +22,7 @@ import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.UnlockSecretKey; import org.pgpainless.util.Passphrase; import sop.SOP; +import sop.exception.SOPGPException; public class GenerateKeyTest { @@ -92,4 +94,20 @@ public class GenerateKeyTest { 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 index 5139bbf6..efcd51c4 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/IncapableKeysTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/IncapableKeysTest.java @@ -13,7 +13,7 @@ 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.EdDSACurve; +import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve; import org.pgpainless.util.ArmorUtils; import sop.SOP; import sop.exception.SOPGPException; @@ -38,15 +38,15 @@ public class IncapableKeysTest { 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(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER)) + .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(EdDSACurve._Ed25519), KeyFlag.SIGN_DATA)) - .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER)) + .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); diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/InlineDetachTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/InlineDetachTest.java index 1054babb..98279e4f 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/InlineDetachTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/InlineDetachTest.java @@ -5,8 +5,6 @@ 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; @@ -34,7 +32,9 @@ 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 { @@ -79,9 +79,11 @@ public class InlineDetachTest { .signatures(signature) .data(message); - assertFalse(verificationList.isEmpty()); - assertEquals(1, verificationList.size()); - assertEquals(new OpenPgpV4Fingerprint(secretKey).toString(), verificationList.get(0).getSigningCertFingerprint()); + VerificationListAssert.assertThatVerificationList(verificationList) + .hasSingleItem() + .issuedBy(new OpenPgpV4Fingerprint(secretKey).toString()) + .hasMode(SignatureMode.text); + assertArrayEquals(data, message); } @@ -121,8 +123,10 @@ public class InlineDetachTest { .signatures(signature) .data(message); - assertFalse(verificationList.isEmpty()); - assertEquals(1, verificationList.size()); + VerificationListAssert.assertThatVerificationList(verificationList) + .hasSingleItem() + .hasMode(SignatureMode.binary); + assertArrayEquals(data, message); } @@ -191,8 +195,10 @@ public class InlineDetachTest { .signatures(signature) .data(message); - assertFalse(verificationList.isEmpty()); - assertEquals(1, verificationList.size()); + 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 index b24b729c..f3a50fc3 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/InlineSignVerifyRoundtripTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/InlineSignVerifyRoundtripTest.java @@ -4,18 +4,19 @@ package org.pgpainless.sop; -import org.junit.jupiter.api.Test; -import sop.ByteArrayAndResult; -import sop.SOP; -import sop.Verification; -import sop.enums.InlineSignAs; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.List; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; +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 { @@ -46,7 +47,11 @@ public class InlineSignVerifyRoundtripTest { byte[] verified = result.getBytes(); - assertFalse(result.getResult().isEmpty()); + List verificationList = result.getResult(); + VerificationListAssert.assertThatVerificationList(verificationList) + .hasSingleItem() + .hasMode(SignatureMode.text); + assertArrayEquals(message, verified); } @@ -65,6 +70,7 @@ public class InlineSignVerifyRoundtripTest { byte[] inlineSigned = sop.inlineSign() .key(key) .withKeyPassword("sw0rdf1sh") + .mode(InlineSignAs.binary) .data(message).getBytes(); ByteArrayAndResult> result = sop.inlineVerify() @@ -74,7 +80,45 @@ public class InlineSignVerifyRoundtripTest { byte[] verified = result.getBytes(); - assertFalse(result.getResult().isEmpty()); + 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/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" + + "lQVYBFDUVsABDADg6AuFsM0JckT7spS/1KNdobaZ1vrOFhGdyXbJ1jUkbMwi+f5o\n" + + "UtQsfeFQRBHeQFfmtt4mo6lE6cAsQFJPFat/ReNxCwCqHi5QbennpbueHJ5N2KVj\n" + + "YrIz6eeTsVKs16gS17zLOMkeBt0TK8+Vu7HHfqLqQ1jNNGujwPydUbO8M431XKeW\n" + + "WhM9ziV9m/20nHYJGIM+aN9AicxtR+khFsNjpRlCMg+8kKUelP2FDWv/5QZwnSXc\n" + + "nMFaCJiH1hx56027AB8PZrUW+ShRhqb0P3EhOt+Gs3IW39rGjc9iQVEWl7745BTZ\n" + + "xEZ4FO84DQQtdKBp510VN8LfiZkO7K9JKOo+vqL4IvSBCJNRVvxDxInShHfVyht7\n" + + "jJJvEC0mxv1Oi8rZ/g9iNd6/Ijthi3svNd3DwNFyMzhrbggynEyWr8nu17Zz0c6C\n" + + "KT7XtFZWOUmio8G14KH6dFRCt7TGRw7mz059ViICMN56Ka5LJaQgGRbT+omY2CQJ\n" + + "q5eSkZMXLndmjtUAEQEAAQAL+QFsyzhLl/oLPs0+63JrTaXPY9s5EpNEkHYTgN29\n" + + "HTUELKdWFBaa7M9sBbCJmiODdEB0mfT+yGW9R6wPCXiaEj8ysMt0QvVzG03Qr6pX\n" + + "kWCmHSuW5ZQHytJjDJMA0T+3K0fQWWFPC/bmX12+1Flw4qI9g6oigub1aF4eJFdV\n" + + "XVq7vhadY9aSIiGtJnX+PqiRIIwPeRDfMjsvwA6H/1dwftltRnbLVr0vnUutRnPv\n" + + "ZGbiOim35bWubLW55Ehycb4T4KyW70Xq0Lljr04/33d7S/SUNHXM/ci2kFDEkJb8\n" + + "N+rssxaVjgPsn9+5wQFDEcrewdMLgaRHSrEf46GvcYMbM8lfnzrDyhYc5+vc24b5\n" + + "85WCVYaYKFrJGEa1vHAMmDwXqDNETtDtaYXZpNsUqvjlG+lU4/p0zeqGfyIDLnzK\n" + + "R5zAmWQkd4aSrgN4F6/7xQ1npnvBq/eZiHJx4sBsPMS10TFPPi3A9jEAiu0eljTq\n" + + "E7eXqDObHD8xSjQ3gm9fBclTUQYA4vVChPT9SgJo2aY5ug+2nLWfv4hgK2LTRNkt\n" + + "exEelaU3vrl83a/HEljg3DZ63P6odRIv0HGRl4YSOEC5CANDcDqjz34p7T6suRU5\n" + + "GzrZHey33joLj9oAGF+2HefmHpvWc8ZzFaS14XiO4m9TMMLZwSokNyhccHl7FSYZ\n" + + "XqxzXD2JnaM+m3XMGRVnASQ2gtmsv8dpXuto+gF/9W1b8kyPp1sjtgup2O4PjiQg\n" + + "1uQMpx6H3OSC8tCH3f9/MvlVTpgtBgD9r5PnN5h6fQd6MQl7UEdTU3jIOcXrr1xh\n" + + "0rQkTQx1WJ29f/tr/sGPb3HgPcpk+x6xctRVpW6yf6OLBP02CnJllBYE73tqIxxa\n" + + "qK+3kDAqIa9n/Ds8SZTH/45JXDFLay5g7kFMpv6dxUUMtdJ8INmcChVPxKeUB5DZ\n" + + "iGMzmCTsR0RxvEIc3ofht7mrMhH361xUZGbIMP6ykZZNlE4FmOW4zBATa8o4V3gl\n" + + "mdbIYopEGPwAuj1gIy0G7fLL0cayEkkF/RI7uep4d2QY87mC+fswbiPWM3mp6/7i\n" + + "e2JLmA2bdDju7SL6X4DMgV8RQakOlQf17JEGA4HrKi3odugiBjdXWv6ZmfcIIPgq\n" + + "ns2Us6wCcr4uqCxEvYj2fUd/q03ui5aglLTqSSuNtnB9yww0EYrj9qjHFIi/ByrF\n" + + "L6DVBrMDJ0BwHY5LkY1OWot4GyjLE43Uqu0ObZhFSMttGQkRxdae0R9+4NPR7Dlw\n" + + "B8+zwytxGRs1NgTy7O+KRl9e3K05bgUXVNsJtBtMZWdhY3kgPGxlZ2FjeUBleGFt\n" + + "cGxlLmNvbT6JAdAEEwECAEQJEJhLkc/TAyFMFiEEIfpuYNbJt9EOrFaimEuRz9MD\n" + + "IUwFglDUVsACngECmwMFFgIDAQAECwkIBwIVAgWJH6AIMQKZAQAAWU0L/jUvlxt0\n" + + "TLLFTcT1tQWvy1MBLJcdiXuoN0/w1Rcz54iSCgWeuNZ5BD6qwCMORmVG1fMuvtCt\n" + + "Lq4NZizE63QfeFE8q22vrNDoZ5pAnjC7KlMMjq1ykQHN7cqH1FgxrS3PrBo1k8/s\n" + + "0P6863Vlso02YYbWluJt4HbnX0vEap4/z05RLBCQyZyiaon5zad5rNd0z1nXfMC8\n" + + "EPRK9MsjBX5/5zhx6RPwCrAlrk5dKZ3Nks6bquTCme8sayBgBHX0Tjeum+3sfwiE\n" + + "Jn2xTYJU6cB7fWYREi9E9z7YrmpVCjDkh8U7p0MLC3dmIYUT3EDL5F0jxTReoX+B\n" + + "7f8HrKUIOyvLlAJs4oxYG/g9QHzVFSAbekwf3Jnwm4Czd6qPx62gI6na11ku64Ua\n" + + "RezZ3NkTInSXi1+Bi7mT4qVcV6Z6vl5YXe8T/Zihcv5/Wp4bNEJ2dHJlhwVAn8Ax\n" + + "Ykl8S2ZVfQ5hN8gWLRW40wnCrbuNUdWI/el9D1arc8AQclXfF8/4kULTq50FWARQ\n" + + "1FbAAQwAv/eK+LYwdkUoGfATB6wcmqaJFrjFIaKYbM1VEWckb4FYc0T1yc9MEq65\n" + + "gz1/PUPt+XwQCa/gP5iCcVuze91ksJVkoeOjy/CQgMD1D1s0IVikVMvOKqdnVa4k\n" + + "SxkLkOvVdzZ5QebDbE5QqfTupyr/SgWarm7TYb4HVFNG5xXVh8+uFMpLe897E+/K\n" + + "mSQMZZ8vdKVvnEm+EOlm0ZzRml4kM8k1LyVxJdoLUJ0t5Ac7B1k/Xq0Fz1Pl3Yjr\n" + + "xahxvz68gTph+uL0IlnxKIt+lI2YKTaZ/QZ6POzif0UHLH4akEoTLjzlzkgNYdiI\n" + + "O3ZekqHViYtlX0brc7TYo3iip1LIvv3NMI7QskA2v9V1NWcf/cPBt0uwJ2wMDDDy\n" + + "bckrrwwsfNn6qFxY3xFo1aexzgpG2C9ZVpIDLMd3F6SUoqrrmAHJLoP0dSYBVujO\n" + + "EAJdPqvLC45KJFgXu6IrBqFrx+WTACJCvgoF8XLLhEba99CwmS8Rc2luS+G3iB8l\n" + + "YQlj5QWXABEBAAEAC/wMe00lhe/f7ZGbIVYun4ahZfnWTyxyI9JPvYh62ZjJSNqD\n" + + "B2IIo/PitLDXObGcpPgQl3wR3sYKT7sOuwZ2ihsFgd38yk8lVktVZwM7SZQGi9VT\n" + + "gu59+eVPV6oaDLmimJ+7YQCNXZj2ewXmDXwe+Aq7ucjCIrtklY7m14Tt4MH1H9z5\n" + + "X3xJw2A4GAiCRvfClV3oJbTJSRPH1Ouch9r3c7uPqm6zPBBmHg4Yr1k2hGNwKa6X\n" + + "IOtJyb8ebzKogJ7n7zo4Cpst01PkdLPnXK3fTEBYjuBQa5F2sSvT89uK3seN3J7W\n" + + "OP05lCcg1k9e4bnD9uGlba0fhsgUhqTEg3za6MNcVezPqRXGXlkWH5gjxbVQHu8B\n" + + "Y8Ix9YvWhCwIA25bSE51bTq2vQuCTaRG5fXVWD8qZ043APcB99c9zW9OvmiJzH47\n" + + "zYk+rB+lByK8/KiaXUqcKjyUniXc9LKda71xb4MwoBuBF9RdCsQvHwFRibdpMd0t\n" + + "a9O7RoTFKPxhUewySoEGANTBWhstEUlsytFMSeNmCmpNR2/mKbuE+n78+zaPCmLF\n" + + "TsLWxil+y3FrJCvffn9k5shtxLADtEvKJKWl/vjXxh9DXzFvMgRPsrETzAkg0zwr\n" + + "+5P8d26x4xcnQaE59RQIhyiJPsT4fXqld+kaKDng0vYkVRGHSIC//NPMPA2KaTdC\n" + + "4EQvEx702dF3/+tIDwXO/kjk6taEEOv0W5nj0aHm+JtEw+X0ja1VvUcDx50Ttwpc\n" + + "LzojtWjFpBNFHLGZyWac4QYA5vx6WsovX9j3YXkYDHbN+r8rCfL/16+z+qEJ/pbw\n" + + "2eevICtB4KLcqXlep4rSLhDJlYphxZfHhsVahwX1ga+fGDB/AuDozJhtfQp7evwN\n" + + "NH5IIAT3o56iBUIO2CywWcdkY+HMo787MbITfvVOdOrGE8hRcCFdkZepaSwfbTRz\n" + + "LZH+jKAU6xOgInuoPVLOOIIlLaTVb6TTRV9BXyRUdele0DqbZIMCwE8P53kFuGuM\n" + + "sRZQ1RNha8H7WU2T2m7QxDl3Bf4+KYQ5AfFPkGZKMQcIJy4CR7hSP9gk3/4B06RM\n" + + "DH3c4rmd50CPpQ5TTA0cGCthOnYVewUgJaxQjKAToX8xCQYFRO59YOc8PMVZ/xgf\n" + + "kGrEkX4tlwECbjoWx2kWT4uZvYmnUzfDdXXr8E+9h2ziEKobF0/b9HQB5BKKLycr\n" + + "KzoTKbV4En1602VltRInAfnjpmQ7VSYV/JyoHJ824d/7O+fLLZkmyibLiSMWPwYu\n" + + "z9rt26lC3cT/HSMrG3L0jjdWH7bYaIkBsAQYAQIAGgWCUNRWwAKeAQKbDAUWAgMB\n" + + "AAQLCQgHAhUCAAoJEJhLkc/TAyFMEn4MAKI6RC+VUJr+p2bMf5Pbfml/iy5QsRBG\n" + + "J1iTyPzu8yJUzHs60y6YckGrIKSFE5x6a6utz/CdtpIlb9e/FJvl82zjxJkFjhre\n" + + "fhHjcu6iIvLCCer6v1XtL4frx6Qoi6TGmlKXWvaLTuRINQFomLwScoHRW1QSQHTE\n" + + "BNUmIo89nRU5PQ8LJBGZWzdkVqVmdbK8ek5ycuolwLUQizbeGIhJo/9IIC2i2RCJ\n" + + "hMVsmbjHB1zdVbwPZuwtCH7ROr4xTLp9Gwq1XcIRYY5am/SyBLgkwKSyrXQs6Zsr\n" + + "2qRd2+ccBF0UYFxvH9JOKmBS6QGwtnAYRqbeeCj8Lx3mgAIv15kGeKd72ezFi0ZT\n" + + "smO3dpb6pSD44BSsdvjZdHENCxYIbBsroDZrZGShygluOCrFjG//PSSbrNE+Bz70\n" + + "imnM2QH/XaS6rpbNPGfrn0Vw5M/ZFT/9PWrEg4ZdCI32ei5uyjYwL7aPAPS3MqkB\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 c9739471..825b5e86 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/VersionTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/VersionTest.java @@ -7,6 +7,7 @@ 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; @@ -49,4 +50,33 @@ public class VersionTest { 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/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/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/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/sop-java-picocli/README.md b/sop-java-picocli/README.md deleted file mode 100644 index d67967b9..00000000 --- a/sop-java-picocli/README.md +++ /dev/null @@ -1,8 +0,0 @@ - -# MOVED -* [Github](https://github.com/pgpainless/sop-java/tree/master/sop-java-picocli) -* [Codeberg](https://codeberg.org/PGPainless/sop-java/src/branch/master/sop-java-picocli) diff --git a/sop-java/README.md b/sop-java/README.md deleted file mode 100644 index dd59c615..00000000 --- a/sop-java/README.md +++ /dev/null @@ -1,8 +0,0 @@ - -# MOVED -* [Github](https://github.com/pgpainless/sop-java/tree/master/sop-java) -* [Codeberg](https://codeberg.org/PGPainless/sop-java/src/branch/master/sop-java) diff --git a/version.gradle b/version.gradle index 2593724d..bc2515a4 100644 --- a/version.gradle +++ b/version.gradle @@ -4,20 +4,15 @@ allprojects { ext { - shortVersion = '1.4.4' - isSnapshot = false - pgpainlessMinAndroidSdk = 10 - javaSourceCompatibility = 1.8 - bouncyCastleVersion = '1.72' - // When using bouncyCastleVersion 1.72: - // unfortunately we rely on 1.72.1 or 1.72.3 for a patch for https://github.com/bcgit/bc-java/issues/1257 - // which is a bug we introduced with a PR against BC :/ oops - // When bouncyCastleVersion is 1.71, bouncyPgVersion can simply be set to 1.71 as well. - bouncyPgVersion = '1.72.3' + shortVersion = '1.7.7' + isSnapshot = true + javaSourceCompatibility = 11 + bouncyCastleVersion = '1.81' + bouncyPgVersion = bouncyCastleVersion junitVersion = '5.8.2' - logbackVersion = '1.2.11' + logbackVersion = '1.5.13' mockitoVersion = '4.5.1' slf4jVersion = '1.7.36' - sopJavaVersion = '4.1.1' + sopJavaVersion = '10.1.1' } }