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 b8726876..d45b16a3 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -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/gradle_push.yml b/.github/workflows/gradle_push.yml index e4624726..8c03a399 100644 --- a/.github/workflows/gradle_push.yml +++ b/.github/workflows/gradle_push.yml @@ -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/.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 4f86b67f..13c93c07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,102 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog -# 1.6.2 +## 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` @@ -13,7 +108,7 @@ SPDX-License-Identifier: CC0-1.0 encrypting to legacy keys which do not carry any key flags. - Allow overriding of reference time in `EncryptionOptions` and `SigningOptions`. -# 1.6.1 +## 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) @@ -27,6 +122,10 @@ SPDX-License-Identifier: CC0-1.0 - `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 @@ -103,6 +202,10 @@ SPDX-License-Identifier: CC0-1.0 - 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 @@ -176,6 +279,10 @@ 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 diff --git a/README.md b/README.md index 659ca896..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/%231/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) @@ -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() @@ -191,7 +191,7 @@ repositories { } dependencies { - implementation 'org.pgpainless:pgpainless-core:1.6.2' + 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 2036180a..a05079d3 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -12,12 +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.5.X | :white_check_mark: | -| 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 d04b6345..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,41 +30,32 @@ allprojects { apply plugin: 'eclipse' apply plugin: 'jacoco' apply plugin: 'checkstyle' - - compileJava { - options.release = 8 - } + 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 = '10.12.1' + 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() @@ -78,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() @@ -111,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 } } @@ -129,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 } @@ -234,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 @@ -245,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') @@ -262,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. * @@ -283,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/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/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 abb86358..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: diff --git a/docs/source/pgpainless-sop/quickstart.md b/docs/source/pgpainless-sop/quickstart.md index 8b8281ac..55958ee4 100644 --- a/docs/source/pgpainless-sop/quickstart.md +++ b/docs/source/pgpainless-sop/quickstart.md @@ -180,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 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/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 98e98857..0d948dfd 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-armor.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-armor.1 @@ -30,16 +30,11 @@ 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 diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-decrypt.1 b/pgpainless-cli/packaging/man/pgpainless-cli-decrypt.1 index 9a3c80a7..eb843e18 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-decrypt.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-decrypt.1 @@ -31,8 +31,8 @@ pgpainless\-cli\-decrypt \- Decrypt a message .SH "SYNOPSIS" .sp \fBpgpainless\-cli decrypt\fP [\fB\-\-stacktrace\fP] [\fB\-\-session\-key\-out\fP=\fISESSIONKEY\fP] -[\fB\-\-verify\-not\-after\fP=\fIDATE\fP] [\fB\-\-verify\-not\-before\fP=\fIDATE\fP] -[\fB\-\-verify\-out\fP=\fIVERIFICATIONS\fP] [\fB\-\-verify\-with\fP=\fICERT\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" @@ -49,7 +49,7 @@ Can be used to learn the session key on successful decryption Print stacktrace .RE .sp -\fB\-\-verify\-not\-after\fP=\fIDATE\fP +\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 @@ -69,11 +69,6 @@ Reject signatures with a creation date not in range. Defaults to beginning of time (\(aq\-\(aq). .RE .sp -\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 diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-encrypt.1 b/pgpainless-cli/packaging/man/pgpainless-cli-encrypt.1 index 29f1d1f1..3cf7aefc 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-encrypt.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-encrypt.1 @@ -31,9 +31,9 @@ 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\-\-sign\-with\fP=\fIKEY\fP]... -[\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP]... [\fB\-\-with\-password\fP=\fIPASSWORD\fP]... -[\fICERTS\fP...] +[\fB\-\-profile\fP=\fIPROFILE\fP] [\fB\-\-session\-key\-out\fP=\fISESSIONKEY\fP] +[\fB\-\-sign\-with\fP=\fIKEY\fP]... [\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP]... +[\fB\-\-with\-password\fP=\fIPASSWORD\fP]... [\fICERTS\fP...] .SH "DESCRIPTION" .SH "OPTIONS" @@ -53,7 +53,7 @@ ASCII armor the output Profile identifier to switch between profiles .RE .sp -\fB\-\-sign\-with\fP=\fIKEY\fP +\fB\-\-session\-key\-out\fP=\fISESSIONKEY\fP, \fB\-\-sign\-with\fP=\fIKEY\fP .RS 4 Sign the output with a private key .RE diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-version.1 b/pgpainless-cli/packaging/man/pgpainless-cli-version.1 index 87db756a..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 | \fB\-\-pgpainless\-cli\-spec\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" @@ -50,7 +50,7 @@ Print an extended version string Print the latest revision of the SOP specification targeted by the implementation .RE .sp -\fB\-\-stacktrace\fP +\fB\-\-sopv\fP, \fB\-\-stacktrace\fP .RS 4 Print stacktrace .RE \ No newline at end of file diff --git a/pgpainless-cli/rewriteManPages.sh b/pgpainless-cli/rewriteManPages.sh index 37b4fdf3..730af68d 100755 --- a/pgpainless-cli/rewriteManPages.sh +++ b/pgpainless-cli/rewriteManPages.sh @@ -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/RoundTripEncryptDecryptCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripEncryptDecryptCmdTest.java index 1d89aeeb..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,7 +14,6 @@ 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; @@ -24,8 +23,8 @@ 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; @@ -83,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"); @@ -304,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); @@ -323,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()); 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 0676f213..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)); } 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 0ff83144..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 @@ -25,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; @@ -202,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/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 830e573d..64d538d5 100644 --- a/pgpainless-core/build.gradle +++ b/pgpainless-core/build.gradle @@ -22,8 +22,16 @@ dependencies { // Bouncy Castle api "org.bouncycastle:bcprov-jdk18on:$bouncyCastleVersion" api "org.bouncycastle:bcpg-jdk18on:$bouncyPgVersion" + api "org.bouncycastle:bcutil-jdk18on:$bouncyCastleVersion" // api(files("../libs/bcpg-jdk18on-1.70.jar")) // @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 3da54177..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java +++ /dev/null @@ -1,238 +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 - */ - @Nonnull - public static KeyRingTemplates generateKeyRing() { - return new KeyRingTemplates(); - } - - /** - * Build a custom OpenPGP key ring. - * - * @return builder - */ - @Nonnull - public static KeyRingBuilder buildKeyRing() { - return new KeyRingBuilder(); - } - - /** - * Read an existing OpenPGP key ring. - * @return builder - */ - @Nonnull - public static KeyRingReader readKeyRing() { - return new KeyRingReader(); - } - - /** - * Extract a public key certificate from a secret key. - * - * @param secretKey secret key - * @return public key certificate - */ - @Nonnull - 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 - */ - @Nonnull - 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} - */ - @Nonnull - 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} - */ - @Nonnull - 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 - */ - @Nonnull - 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 - */ - @Nonnull - 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 - */ - @Nonnull - public static SecretKeyRingEditorInterface modifyKeyRing(@Nonnull PGPSecretKeyRing secretKeys) { - return modifyKeyRing(secretKeys, new Date()); - } - - /** - * 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 - */ - @Nonnull - public static SecretKeyRingEditorInterface modifyKeyRing(@Nonnull PGPSecretKeyRing secretKeys, - @Nonnull 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 - */ - @Nonnull - public static KeyRingInfo inspectKeyRing(@Nonnull 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 - */ - @Nonnull - public static KeyRingInfo inspectKeyRing(@Nonnull PGPKeyRing keyRing, @Nonnull Date referenceTime) { - return new KeyRingInfo(keyRing, referenceTime); - } - - /** - * Access, and make changes to PGPainless policy on acceptable/default algorithms etc. - * - * @return policy - */ - @Nonnull - public static Policy getPolicy() { - return Policy.getInstance(); - } - - /** - * Create different kinds of signatures on other keys. - * - * @return builder - */ - @Nonnull - 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 d9aa9b90..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 - */ - GNUPG_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 - */ - GNUPG_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/authentication/CertificateAuthenticity.java b/pgpainless-core/src/main/java/org/pgpainless/authentication/CertificateAuthenticity.java deleted file mode 100644 index ac059646..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/authentication/CertificateAuthenticity.java +++ /dev/null @@ -1,139 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.authentication; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import javax.annotation.Nonnull; - -import org.bouncycastle.openpgp.PGPPublicKeyRing; - -public class CertificateAuthenticity { - - private final String userId; - private final PGPPublicKeyRing certificate; - private final Map certificationChains = new HashMap<>(); - private final int targetAmount; - - public CertificateAuthenticity(@Nonnull PGPPublicKeyRing certificate, - @Nonnull String userId, - @Nonnull Map certificationChains, - int targetAmount) { - this.userId = userId; - this.certificate = certificate; - this.certificationChains.putAll(certificationChains); - this.targetAmount = targetAmount; - } - - @Nonnull - public String getUserId() { - return userId; - } - - @Nonnull - public PGPPublicKeyRing getCertificate() { - return certificate; - } - - public int getTotalTrustAmount() { - int total = 0; - for (int v : certificationChains.values()) { - total += v; - } - return total; - } - - /** - * 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 - */ - public int getAuthenticityPercentage() { - return targetAmount * 100 / getTotalTrustAmount(); - } - - /** - * Return true, if the binding is authenticated to a sufficient degree. - * - * @return true if total gathered evidence outweighs the target trust amount. - */ - public boolean isAuthenticated() { - return targetAmount <= getTotalTrustAmount(); - } - - /** - * Return a map of {@link CertificationChain CertificationChains} and their respective effective trust amount. - * The effective trust amount of a path might be smaller than its actual trust amount, for example if nodes of a - * path are used multiple times. - * - * @return map of certification chains and their effective trust amounts - */ - @Nonnull - public Map getCertificationChains() { - return Collections.unmodifiableMap(certificationChains); - } - - public static class CertificationChain { - private final int trustAmount; - private final List chainLinks = new ArrayList<>(); - - /** - * 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. - */ - public CertificationChain(int trustAmount, @Nonnull List chainLinks) { - this.trustAmount = trustAmount; - this.chainLinks.addAll(chainLinks); - } - - /** - * Actual trust amount of the certification chain. - * @return trust amount - */ - public int getTrustAmount() { - return trustAmount; - } - - /** - * Return all links in the chain, starting at the trust-root and ending at the target. - * @return chain links - */ - @Nonnull - public List getChainLinks() { - return Collections.unmodifiableList(chainLinks); - } - } - - /** - * A chain link contains a node in the trust chain. - */ - public static class ChainLink { - private final PGPPublicKeyRing certificate; - - /** - * Create a chain link. - * @param certificate node in the trust chain - */ - public ChainLink(@Nonnull PGPPublicKeyRing certificate) { - this.certificate = certificate; - } - - /** - * Return the certificate that belongs to the node. - * @return certificate - */ - @Nonnull - public PGPPublicKeyRing getCertificate() { - return certificate; - } - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/authentication/CertificateAuthority.java b/pgpainless-core/src/main/java/org/pgpainless/authentication/CertificateAuthority.java deleted file mode 100644 index 36bf9e5f..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/authentication/CertificateAuthority.java +++ /dev/null @@ -1,71 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.authentication; - -import org.pgpainless.key.OpenPgpFingerprint; - -import javax.annotation.Nonnull; -import java.util.Date; -import java.util.List; - -/** - * 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 - */ -public 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 - */ - CertificateAuthenticity authenticateBinding(@Nonnull OpenPgpFingerprint fingerprint, - @Nonnull String userId, - boolean email, - @Nonnull Date referenceTime, - int targetAmount); - - /** - * 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 - */ - List lookupByUserId(@Nonnull String userId, - boolean email, - @Nonnull Date referenceTime, - int targetAmount); - - /** - * 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 - */ - List identifyByFingerprint(@Nonnull OpenPgpFingerprint fingerprint, - @Nonnull Date referenceTime, - int targetAmount); -} 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 fc88ef33..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java +++ /dev/null @@ -1,105 +0,0 @@ -// SPDX-FileCopyrightText: 2022 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.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. - */ -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(AEADEncDataPacket aeadEncDataPacket, PGPSessionKey sessionKey) - throws PGPException { - return factory.createDataDecryptor(aeadEncDataPacket, sessionKey); - } - - @Override - public PGPDataDecryptor createDataDecryptor(SymmetricEncIntegrityPacket seipd, PGPSessionKey sessionKey) - throws PGPException { - return factory.createDataDecryptor(seipd, sessionKey); - } - - @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 041a5437..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java +++ /dev/null @@ -1,858 +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.authentication.CertificateAuthenticity; -import org.pgpainless.authentication.CertificateAuthority; -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.6.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; - } - - /** - * 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 - * @return true, if we can authenticate a binding for a signing key with sufficient evidence - */ - public boolean isAuthenticatablySignedBy(String userId, boolean email, CertificateAuthority certificateAuthority) { - return isAuthenticatablySignedBy(userId, email, certificateAuthority, 120); - } - - /** - * Return true, if the message was verifiably 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 target trust amount - * @return true, if we can authenticate a binding for a signing key with sufficient evidence - */ - public boolean isAuthenticatablySignedBy(String userId, boolean email, CertificateAuthority certificateAuthority, int targetAmount) { - for (SignatureVerification verification : getVerifiedSignatures()) { - CertificateAuthenticity authenticity = certificateAuthority.authenticateBinding( - verification.getSigningKey().getFingerprint(), userId, email, - verification.getSignature().getCreationTime(), targetAmount); - if (authenticity.isAuthenticated()) { - return true; - } - } - return false; - } - - /** - * Return a list containing all recipient keyIDs. - * - * @return list of recipients - */ - public List getRecipientKeyIds() { - List keyIds = new ArrayList<>(); - Iterator encLayers = getEncryptionLayers(); - while (encLayers.hasNext()) { - EncryptedData layer = encLayers.next(); - keyIds.addAll(layer.getRecipients()); - } - return keyIds; - } - - 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 ff020a7b..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; @@ -208,8 +209,8 @@ public class OpenPgpInputStream extends BufferedInputStream { } try { - SignatureType.valueOf(sigType); - } catch (IllegalArgumentException e) { + SignatureType.requireFromCode(sigType); + } catch (NoSuchElementException e) { return; } @@ -236,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 b9ead7e7..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ /dev/null @@ -1,1139 +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.OpenPgpPacket; -import org.pgpainless.algorithm.StreamEncoding; -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; -import org.pgpainless.decryption_verification.cleartext_signatures.ClearsignedMessageUtil; -import org.pgpainless.decryption_verification.cleartext_signatures.MultiPassStrategy; -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.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.requireFromId(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()); - assert (keys != null); - 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); - assert (decryptionKey != null); - 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()); - } - for (PGPPublicKeyEncryptedData pkesk : esks.anonPkesks) { - 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.getDecryptionSubkeys()) { - 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.getDecryptionSubkeys(); - 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.next(InputSymbol.EndOfSequence); - 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.next(InputSymbol.EndOfSequence); - 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 e2d5f1ca..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.6.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 725c6f6e..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java +++ /dev/null @@ -1,159 +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 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; - -import javax.annotation.Nonnull; - -/** - * 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; - } - - return OpenPgpPacket.requireFromTag(tag); - } - - 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 (e.getMessage().contains("crc check failed in armored message")) { - throw e; - } - return -1; - } - } - - @Override - public int read(@Nonnull 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 9d20e0a8..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.java +++ /dev/null @@ -1,142 +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 { - if (input == InputSymbol.EndOfSequence) { - // allow subsequent read() calls. - return new Transition(State.Valid); - } - // 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 5cdf9e36..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/BcPGPHashContextContentSignerBuilder.java +++ /dev/null @@ -1,174 +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 = requireFromName(messageDigest.getAlgorithm()); - } - - private static HashAlgorithm requireFromName(String digestName) { - HashAlgorithm hashAlgorithm = HashAlgorithm.fromName(digestName); - if (hashAlgorithm == null) { - throw new IllegalArgumentException("Cannot recognize OpenPGP Hash Algorithm: " + digestName); - } - return hashAlgorithm; - } - - @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 bb937a1a..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java +++ /dev/null @@ -1,473 +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.bouncycastle.openpgp.operator.PublicKeyKeyEncryptionMethodGenerator; -import org.pgpainless.algorithm.EncryptionPurpose; -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; -import org.pgpainless.authentication.CertificateAuthenticity; -import org.pgpainless.authentication.CertificateAuthority; -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 boolean allowEncryptionWithMissingKeyFlags = false; - private Date evaluationDate = new Date(); - - 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(@Nonnull 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(); - } - - /** - * Override the evaluation date for recipient keys with the given date. - * - * @param evaluationDate new evaluation date - * @return this - */ - public EncryptionOptions setEvaluationDate(@Nonnull Date evaluationDate) { - this.evaluationDate = evaluationDate; - return this; - } - - /** - * 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); - } - - /** - * Identify authenticatable certificates for the given user-ID by querying the {@link 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 - * @return encryption options - */ - public EncryptionOptions addAuthenticatableRecipients(String userId, boolean email, CertificateAuthority authority) { - return addAuthenticatableRecipients(userId, email, authority, 120); - } - - /** - * Identify authenticatable certificates for the given user-ID by querying the {@link 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 - */ - public EncryptionOptions addAuthenticatableRecipients(String userId, boolean email, CertificateAuthority authority, int targetAmount) { - List identifiedCertificates = authority.lookupByUserId(userId, email, evaluationDate, targetAmount); - boolean foundAcceptable = false; - for (CertificateAuthenticity candidate : identifiedCertificates) { - if (candidate.isAuthenticated()) { - addRecipient(candidate.getCertificate()); - foundAcceptable = true; - } - } - if (!foundAcceptable) { - throw new IllegalArgumentException("Could not identify any trust-worthy certificates for '" + userId + "' and target trust amount " + targetAmount); - } - return this; - } - - /** - * Add all key rings in the provided {@link Iterable} (e.g. {@link PGPPublicKeyRingCollection}) as recipients. - * - * @param keys keys - * @return this - */ - public EncryptionOptions addRecipients(@Nonnull 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(@Nonnull PGPPublicKeyRing key, @Nonnull CharSequence 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(@Nonnull PGPPublicKeyRing key, - @Nonnull CharSequence userId, - @Nonnull EncryptionKeySelector encryptionKeySelectionStrategy) { - KeyRingInfo info = new KeyRingInfo(key, evaluationDate); - - List encryptionSubkeys = encryptionKeySelectionStrategy - .selectEncryptionSubkeys(info.getEncryptionSubkeys(userId.toString(), 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.toString())); - addRecipientKey(key, encryptionSubkey, false); - } - - return this; - } - - /** - * Add a recipient by providing a key. - * - * @param key key ring - * @return this - */ - public EncryptionOptions addRecipient(@Nonnull 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(@Nonnull PGPPublicKeyRing key, - @Nonnull EncryptionKeySelector encryptionKeySelectionStrategy) { - return addAsRecipient(key, encryptionKeySelectionStrategy, false); - } - - /** - * Add a certificate as hidden recipient. - * The recipients key-id will be obfuscated by setting a wildcard key ID. - * - * @param key recipient key - * @return this - */ - public EncryptionOptions addHiddenRecipient(@Nonnull PGPPublicKeyRing key) { - return addHiddenRecipient(key, encryptionKeySelector); - } - - /** - * Add a certificate as hidden recipient, using the provided {@link EncryptionKeySelector} to select recipient subkeys. - * The recipients key-ids will be obfuscated by setting a wildcard key ID instead. - * - * @param key recipient key - * @param encryptionKeySelectionStrategy strategy to select recipient (sub) keys. - * @return this - */ - public EncryptionOptions addHiddenRecipient(PGPPublicKeyRing key, EncryptionKeySelector encryptionKeySelectionStrategy) { - return addAsRecipient(key, encryptionKeySelectionStrategy, true); - } - - private EncryptionOptions addAsRecipient(PGPPublicKeyRing key, EncryptionKeySelector encryptionKeySelectionStrategy, boolean wildcardKeyId) { - KeyRingInfo 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)); - - // 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) { - List validSubkeys = info.getValidSubkeys(); - for (PGPPublicKey validSubkey : validSubkeys) { - if (!validSubkey.isEncryptionKey()) { - continue; - } - // only add encryption keys with no key flags. - if (info.getKeyFlagsOf(validSubkey.getKeyID()).isEmpty()) { - encryptionSubkeys.add(validSubkey); - } - } - } - - 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, wildcardKeyId); - } - - return this; - } - - private void addRecipientKey(@Nonnull PGPPublicKeyRing keyRing, - @Nonnull PGPPublicKey key, - boolean wildcardKeyId) { - encryptionKeys.add(new SubkeyIdentifier(keyRing, key.getKeyID())); - PublicKeyKeyEncryptionMethodGenerator encryptionMethod = ImplementationFactory - .getInstance().getPublicKeyKeyEncryptionMethodGenerator(key); - encryptionMethod.setUseWildcardKeyID(wildcardKeyId); - addEncryptionMethod(encryptionMethod); - } - - /** - * Add a symmetric passphrase which the message will be encrypted to. - * - * @param passphrase passphrase - * @return this - */ - public EncryptionOptions addPassphrase(@Nonnull 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(@Nonnull 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(@Nonnull 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; - } - - /** - * If this method is called, subsequent calls to {@link #addRecipient(PGPPublicKeyRing)} will allow encryption - * for subkeys that do not carry any {@link 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 - */ - public EncryptionOptions setAllowEncryptionWithMissingKeyFlags() { - this.allowEncryptionWithMissingKeyFlags = true; - 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(@Nonnull 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(@Nonnull 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(@Nonnull 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 7af5a7b3..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java +++ /dev/null @@ -1,313 +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.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); - 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 7b8529fe..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/PGPHashContextContentSignerBuilder.java +++ /dev/null @@ -1,83 +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 javax.annotation.Nonnull; - -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(@Nonnull byte[] bytes, int off, int len) { - sig.update(bytes, off, len); - } - - public void write(@Nonnull byte[] bytes) { - sig.update(bytes, 0, bytes.length); - } - - public void write(int b) { - 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 00fbb10a..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/ProducerOptions.java +++ /dev/null @@ -1,355 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.encryption_signing; - -import java.util.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 String version = 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) { - this.comment = comment; - return this; - } - - /** - * 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 - * {@link org.pgpainless.util.ArmoredOutputStreamFactory#setVersionInfo(String)}. - * - * @param version version header, or null for no version info. - * @return builder - */ - public ProducerOptions setVersion(String version) { - this.version = version; - return this; - } - - /** - * Return comment set for header in ascii armored output. - * - * @return comment - */ - public String getComment() { - return comment; - } - - /** - * Return the version info header in ascii armored output. - * - * @return version info - */ - public String getVersion() { - return version; - } - - /** - * Return whether a comment was set (!= null). - * - * @return true if commend is set - */ - public boolean hasComment() { - return comment != null; - } - - /** - * Return whether a version header was set (!= null). - * - * @return true if version header is set - */ - public boolean hasVersion() { - return version != 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 34d4bbcf..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java +++ /dev/null @@ -1,634 +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.Nonnull; -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(@Nonnull PGPSignatureGenerator signatureGenerator, - boolean detached, - @Nonnull 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(@Nonnull PGPSignatureGenerator signatureGenerator, - @Nonnull 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(@Nonnull PGPSignatureGenerator signatureGenerator, - @Nonnull 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; - private Date evaluationDate = new Date(); - - @Nonnull - public static SigningOptions get() { - return new SigningOptions(); - } - - /** - * Override the evaluation date for signing keys with the given date. - * - * @param evaluationDate new evaluation date - * @return this - */ - public SigningOptions setEvaluationDate(@Nonnull Date evaluationDate) { - this.evaluationDate = evaluationDate; - return this; - } - - /** - * 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 - */ - @Nonnull - public SigningOptions addSignature(@Nonnull SecretKeyRingProtector signingKeyProtector, - @Nonnull 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 - */ - @Nonnull - public SigningOptions addInlineSignatures(@Nonnull SecretKeyRingProtector secrectKeyDecryptor, - @Nonnull Iterable signingKeys, - @Nonnull 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 - */ - @Nonnull - public SigningOptions addInlineSignature(@Nonnull SecretKeyRingProtector secretKeyDecryptor, - @Nonnull PGPSecretKeyRing secretKey, - @Nonnull 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 - */ - @Nonnull - public SigningOptions addInlineSignature(@Nonnull SecretKeyRingProtector secretKeyDecryptor, - @Nonnull PGPSecretKeyRing secretKey, - @Nullable CharSequence userId, - @Nonnull 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 - */ - @Nonnull - public SigningOptions addInlineSignature(@Nonnull SecretKeyRingProtector secretKeyDecryptor, - @Nonnull PGPSecretKeyRing secretKey, - @Nullable CharSequence userId, - @Nonnull DocumentSignatureType signatureType, - @Nullable BaseSignatureSubpackets.Callback subpacketsCallback) - throws KeyException, PGPException { - KeyRingInfo keyRingInfo = PGPainless.inspectKeyRing(secretKey, evaluationDate); - if (userId != null && !keyRingInfo.isUserIdValid(userId)) { - throw new KeyException.UnboundUserIdException( - OpenPgpFingerprint.of(secretKey), - userId.toString(), - 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; - } - - /** - * Create a binary inline signature using the signing key with the given keyId. - * - * @param secretKeyDecryptor decryptor to unlock the secret key - * @param secretKey secret key ring - * @param keyId keyId of the signing (sub-)key - * @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 - */ - @Nonnull - public SigningOptions addInlineSignature(@Nonnull SecretKeyRingProtector secretKeyDecryptor, - @Nonnull PGPSecretKeyRing secretKey, - long keyId) throws PGPException { - return addInlineSignature(secretKeyDecryptor, secretKey, keyId, DocumentSignatureType.BINARY_DOCUMENT, null); - } - - - /** - * Create an inline signature using the signing key with the given keyId. - * - * @param secretKeyDecryptor decryptor to unlock the secret key - * @param secretKey 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 - */ - @Nonnull - public SigningOptions addInlineSignature(@Nonnull SecretKeyRingProtector secretKeyDecryptor, - @Nonnull PGPSecretKeyRing secretKey, - long keyId, - @Nonnull DocumentSignatureType signatureType, - @Nullable BaseSignatureSubpackets.Callback subpacketsCallback) throws PGPException { - KeyRingInfo keyRingInfo = PGPainless.inspectKeyRing(secretKey, evaluationDate); - - List signingPubKeys = keyRingInfo.getSigningSubkeys(); - if (signingPubKeys.isEmpty()) { - throw new KeyException.UnacceptableSigningKeyException(OpenPgpFingerprint.of(secretKey)); - } - - for (PGPPublicKey signingPubKey : signingPubKeys) { - if (signingPubKey.getKeyID() == keyId) { - - 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 = keyRingInfo.getPreferredHashAlgorithms(signingPubKey.getKeyID()); - HashAlgorithm hashAlgorithm = negotiateHashAlgorithm(hashAlgorithms, PGPainless.getPolicy()); - addSigningMethod(secretKey, signingSubkey, subpacketsCallback, hashAlgorithm, signatureType, false); - return this; - } - } - - throw new KeyException.MissingSecretKeyException(OpenPgpFingerprint.of(secretKey), keyId); - } - - /** - * 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 - */ - @Nonnull - public SigningOptions addDetachedSignatures(@Nonnull SecretKeyRingProtector secretKeyDecryptor, - @Nonnull Iterable signingKeys, - @Nonnull 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 - */ - @Nonnull - public SigningOptions addDetachedSignature(@Nonnull SecretKeyRingProtector secretKeyDecryptor, - @Nonnull 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 - */ - @Nonnull - public SigningOptions addDetachedSignature(@Nonnull SecretKeyRingProtector secretKeyDecryptor, - @Nonnull PGPSecretKeyRing secretKey, - @Nonnull 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 - */ - @Nonnull - public SigningOptions addDetachedSignature(@Nonnull SecretKeyRingProtector secretKeyDecryptor, - @Nonnull PGPSecretKeyRing secretKey, - @Nullable CharSequence userId, - @Nonnull 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 - */ - @Nonnull - public SigningOptions addDetachedSignature(@Nonnull SecretKeyRingProtector secretKeyDecryptor, - @Nonnull PGPSecretKeyRing secretKey, - @Nullable CharSequence userId, - @Nonnull DocumentSignatureType signatureType, - @Nullable BaseSignatureSubpackets.Callback subpacketCallback) - throws PGPException { - KeyRingInfo keyRingInfo = PGPainless.inspectKeyRing(secretKey, evaluationDate); - if (userId != null && !keyRingInfo.isUserIdValid(userId)) { - throw new KeyException.UnboundUserIdException( - OpenPgpFingerprint.of(secretKey), - userId.toString(), - 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; - } - - /** - * Create a detached binary signature using the signing key with the given keyId. - * - * @param secretKeyDecryptor decryptor to unlock the secret key - * @param secretKey secret key ring - * @param keyId keyId of the signing (sub-)key - * @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 - */ - @Nonnull - public SigningOptions addDetachedSignature(@Nonnull SecretKeyRingProtector secretKeyDecryptor, - @Nonnull PGPSecretKeyRing secretKey, - long keyId) throws PGPException { - return addDetachedSignature(secretKeyDecryptor, secretKey, keyId, DocumentSignatureType.BINARY_DOCUMENT, null); - } - - /** - * Create a detached signature using the signing key with the given keyId. - * - * @param secretKeyDecryptor decryptor to unlock the secret key - * @param secretKey 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 - */ - @Nonnull - public SigningOptions addDetachedSignature(@Nonnull SecretKeyRingProtector secretKeyDecryptor, - @Nonnull PGPSecretKeyRing secretKey, - long keyId, - @Nonnull DocumentSignatureType signatureType, - @Nullable BaseSignatureSubpackets.Callback subpacketsCallback) throws PGPException { - KeyRingInfo keyRingInfo = PGPainless.inspectKeyRing(secretKey, evaluationDate); - - List signingPubKeys = keyRingInfo.getSigningSubkeys(); - if (signingPubKeys.isEmpty()) { - throw new KeyException.UnacceptableSigningKeyException(OpenPgpFingerprint.of(secretKey)); - } - - for (PGPPublicKey signingPubKey : signingPubKeys) { - if (signingPubKey.getKeyID() == keyId) { - - 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 = keyRingInfo.getPreferredHashAlgorithms(signingPubKey.getKeyID()); - HashAlgorithm hashAlgorithm = negotiateHashAlgorithm(hashAlgorithms, PGPainless.getPolicy()); - addSigningMethod(secretKey, signingSubkey, subpacketsCallback, hashAlgorithm, signatureType, true); - return this; - } - } - - throw new KeyException.MissingSecretKeyException(OpenPgpFingerprint.of(secretKey), keyId); - } - - private void addSigningMethod(@Nonnull PGPSecretKeyRing secretKey, - @Nonnull PGPPrivateKey signingSubkey, - @Nullable BaseSignatureSubpackets.Callback subpacketCallback, - @Nonnull HashAlgorithm hashAlgorithm, - @Nonnull 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 = signingSecretKey.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 - */ - @Nonnull - private HashAlgorithm negotiateHashAlgorithm(@Nonnull Set preferences, - @Nonnull Policy policy) { - if (hashAlgorithmOverride != null) { - return hashAlgorithmOverride; - } - - return HashAlgorithmNegotiator.negotiateSignatureHashAlgorithm(policy) - .negotiateHashAlgorithm(preferences); - } - - @Nonnull - private PGPSignatureGenerator createSignatureGenerator(@Nonnull PGPPrivateKey privateKey, - @Nonnull HashAlgorithm hashAlgorithm, - @Nonnull 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 - */ - @Nonnull - 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 - */ - @Nonnull - public SigningOptions overrideHashAlgorithm(@Nonnull HashAlgorithm hashAlgorithmOverride) { - this.hashAlgorithmOverride = hashAlgorithmOverride; - return this; - } - - /** - * Return the hash algorithm override (or null if no override is set). - * - * @return hash algorithm override - */ - @Nullable - 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 cbc320e6..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/implementation/BcImplementationFactory.java +++ /dev/null @@ -1,154 +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.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(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 90d1330f..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/implementation/ImplementationFactory.java +++ /dev/null @@ -1,119 +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.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 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 10760de2..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/implementation/JceImplementationFactory.java +++ /dev/null @@ -1,141 +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.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.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(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 PGPObjectFactory(inputStream, ImplementationFactory.getInstance().getKeyFingerprintCalculator()); - } - - @Override - public PGPObjectFactory getPGPObjectFactory(byte[] bytes) { - return new PGPObjectFactory(bytes, ImplementationFactory.getInstance().getKeyFingerprintCalculator()); - } -} 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 13804061..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpFingerprint.java +++ /dev/null @@ -1,189 +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.PGPSecretKey; -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(PGPSecretKey key) { - return of(key.getPublicKey()); - } - - /** - * 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); - } - if (key.getVersion() == 6) { - return new OpenPgpV6Fingerprint(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. - * If the trimmed fingerprint without whitespace is 64 characters long, it is either a v5 or v6 fingerprint. - * In this case, we return a {@link _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. - */ - @Deprecated - 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}$")) { - // Might be v5 or v6 :/ - return new _64DigitFingerprint(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 - * @deprecated use the parse() methods of the versioned fingerprint subclasses instead. - */ - @Deprecated - 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 a0a3d1f4..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpV5Fingerprint.java +++ /dev/null @@ -1,58 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key; - -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; - -/** - * This class represents a hex encoded, upper case OpenPGP v5 fingerprint. - */ -public class OpenPgpV5Fingerprint extends _64DigitFingerprint { - - /** - * 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(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; - } - -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpV6Fingerprint.java b/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpV6Fingerprint.java deleted file mode 100644 index 79cc1715..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpV6Fingerprint.java +++ /dev/null @@ -1,58 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key; - -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; - -/** - * This class represents a hex encoded, upper case OpenPGP v6 fingerprint. - */ -public class OpenPgpV6Fingerprint extends _64DigitFingerprint { - - /** - * Create an {@link OpenPgpV6Fingerprint}. - * - * @param fingerprint uppercase hexadecimal fingerprint of length 64 - */ - public OpenPgpV6Fingerprint(@Nonnull String fingerprint) { - super(fingerprint); - } - - public OpenPgpV6Fingerprint(@Nonnull byte[] bytes) { - super(bytes); - } - - public OpenPgpV6Fingerprint(@Nonnull PGPPublicKey key) { - super(key); - } - - public OpenPgpV6Fingerprint(@Nonnull PGPSecretKey key) { - this(key.getPublicKey()); - } - - public OpenPgpV6Fingerprint(@Nonnull PGPPublicKeyRing ring) { - super(ring); - } - - public OpenPgpV6Fingerprint(@Nonnull PGPSecretKeyRing ring) { - super(ring); - } - - public OpenPgpV6Fingerprint(@Nonnull PGPKeyRing ring) { - super(ring); - } - - @Override - public int getVersion() { - return 6; - } - -} 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/_64DigitFingerprint.java b/pgpainless-core/src/main/java/org/pgpainless/key/_64DigitFingerprint.java deleted file mode 100644 index 11f18058..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/_64DigitFingerprint.java +++ /dev/null @@ -1,119 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key; - -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, 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. - */ -public class _64DigitFingerprint extends OpenPgpFingerprint { - - /** - * Create an {@link _64DigitFingerprint}. - * - * @param fingerprint uppercase hexadecimal fingerprint of length 64 - */ - protected _64DigitFingerprint(@Nonnull String fingerprint) { - super(fingerprint); - } - - protected _64DigitFingerprint(@Nonnull byte[] bytes) { - super(Hex.encode(bytes)); - } - - protected _64DigitFingerprint(@Nonnull PGPPublicKey key) { - super(key); - } - - protected _64DigitFingerprint(@Nonnull PGPSecretKey key) { - this(key.getPublicKey()); - } - - protected _64DigitFingerprint(@Nonnull PGPPublicKeyRing ring) { - super(ring); - } - - protected _64DigitFingerprint(@Nonnull PGPSecretKeyRing ring) { - super(ring); - } - - protected _64DigitFingerprint(@Nonnull PGPKeyRing ring) { - super(ring); - } - - @Override - public int getVersion() { - return -1; // might be v5 or v6 - } - - @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/certification/CertifyCertificate.java b/pgpainless-core/src/main/java/org/pgpainless/key/certification/CertifyCertificate.java deleted file mode 100644 index 9340d93d..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/certification/CertifyCertificate.java +++ /dev/null @@ -1,294 +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); - assert (certificationPubKey != null); - 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 208c17b6..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingBuilder.java +++ /dev/null @@ -1,322 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.generation; - - -import java.io.IOException; -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.bouncycastle.util.Strings; -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 { - - private static final long YEAR_IN_SECONDS = 1000L * 60 * 60 * 24 * 365; - - private KeySpec primaryKeySpec; - private final List subkeySpecs = new ArrayList<>(); - private final Map userIds = new LinkedHashMap<>(); - private Passphrase passphrase = Passphrase.emptyPassphrase(); - private Date expirationDate = new Date(System.currentTimeMillis() + YEAR_IN_SECONDS * 5); // Expiration in 5 years - - @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(Strings.fromUTF8ByteArray(userId)); - } - - @Override - public KeyRingBuilder setExpirationDate(@Nullable Date expirationDate) { - if (expirationDate == null) { - // No expiration - this.expirationDate = null; - return this; - } - - 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 6966232b..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingTemplates.java +++ /dev/null @@ -1,255 +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.util.Passphrase; - -public final class KeyRingTemplates { - - public 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 - * @return key - * @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 rsaKeyRing(@Nullable CharSequence userId, - @Nonnull RsaLength length) - throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - return rsaKeyRing(userId, length, Passphrase.emptyPassphrase()); - } - - /** - * 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 - * @return key - * @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 rsaKeyRing(@Nullable CharSequence userId, - @Nonnull RsaLength length, - @Nonnull String password) - throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - Passphrase passphrase = Passphrase.emptyPassphrase(); - if (!isNullOrEmpty(password)) { - passphrase = Passphrase.fromPassword(password); - } - return rsaKeyRing(userId, length, passphrase); - } - - /** - * 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 - * @return key - * @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 rsaKeyRing(@Nullable CharSequence userId, - @Nonnull RsaLength length, - @Nonnull Passphrase passphrase) - throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - KeyRingBuilder builder = PGPainless.buildKeyRing() - .setPrimaryKey(KeySpec.getBuilder(KeyType.RSA(length), KeyFlag.CERTIFY_OTHER)) - .addSubkey(KeySpec.getBuilder(KeyType.RSA(length), KeyFlag.SIGN_DATA)) - .addSubkey(KeySpec.getBuilder(KeyType.RSA(length), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)); - - if (userId != null) { - builder.addUserId(userId.toString()); - } - - if (!passphrase.isEmpty()) { - builder.setPassphrase(passphrase); - } - - return builder.build(); - } - - /** - * 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 CharSequence userId, @Nonnull RsaLength length) - throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { - return simpleRsaKeyRing(userId, length, Passphrase.emptyPassphrase()); - } - - public PGPSecretKeyRing simpleRsaKeyRing(@Nullable CharSequence 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.toString()); - } - return builder.build(); - } - - /** - * 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 CharSequence 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); - } - - /** - * 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 CharSequence userId) - throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { - return simpleEcKeyRing(userId, Passphrase.emptyPassphrase()); - } - - /** - * 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 CharSequence 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 CharSequence 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.toString()); - } - 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 CharSequence 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 CharSequence 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 CharSequence 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.toString()); - } - 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 2db57f57..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/eddsa/EdDSA.java +++ /dev/null @@ -1,50 +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). - * - * @see EdDSA for OpenPGP - */ -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 23c33d3d..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/elgamal/ElGamal.java +++ /dev/null @@ -1,52 +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. - * - * @deprecated the use of ElGamal is not recommended anymore. - */ -@Deprecated -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 17e79131..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/elgamal/ElGamalLength.java +++ /dev/null @@ -1,81 +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) - * - * @deprecated the use of ElGamal keys is no longer recommended. - */ -@Deprecated -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 8ab8a9c4..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyAccessor.java +++ /dev/null @@ -1,167 +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(@Nonnull KeyRingInfo info, @Nonnull 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 - */ - @Nonnull - public abstract PGPSignature getSignatureWithPreferences(); - - /** - * Return preferred symmetric key encryption algorithms. - * - * @return preferred symmetric algorithms - */ - @Nonnull - public Set getPreferredSymmetricKeyAlgorithms() { - return SignatureSubpacketsUtil.parsePreferredSymmetricKeyAlgorithms(getSignatureWithPreferences()); - } - - /** - * Return preferred hash algorithms. - * - * @return preferred hash algorithms - */ - @Nonnull - public Set getPreferredHashAlgorithms() { - return SignatureSubpacketsUtil.parsePreferredHashAlgorithms(getSignatureWithPreferences()); - } - - /** - * Return preferred compression algorithms. - * - * @return preferred compression algorithms - */ - @Nonnull - 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(@Nonnull KeyRingInfo info, - @Nonnull SubkeyIdentifier key, - @Nonnull String userId) { - super(info, key); - this.userId = userId; - } - - @Override - @Nonnull - public 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(@Nonnull KeyRingInfo info, - @Nonnull SubkeyIdentifier key) { - super(info, key); - } - - @Override - @Nonnull - public 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 = null; - if (primaryUserId != null) { - 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(@Nonnull KeyRingInfo info, - @Nonnull SubkeyIdentifier key) { - super(info, key); - } - - @Override - @Nonnull - public PGPSignature getSignatureWithPreferences() { - PGPSignature signature; - if (key.getPrimaryKeyId() == key.getSubkeyId()) { - signature = info.getLatestDirectKeySelfSignature(); - if (signature == null && info.getPrimaryUserId() != null) { - signature = info.getLatestUserIdCertification(info.getPrimaryUserId()); - } - } else { - signature = info.getCurrentSubkeyBindingSignature(key.getSubkeyId()); - } - - if (signature == null) { - throw new IllegalStateException("No valid signature found."); - } - return signature; - } - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyInfo.java b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyInfo.java deleted file mode 100644 index f455608e..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 once BC 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 77277622..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java +++ /dev/null @@ -1,1314 +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.KeyIdUtil; -import org.pgpainless.key.util.KeyRingUtils; -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; -import org.pgpainless.util.DateUtil; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * 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 static final Logger LOGGER = LoggerFactory.getLogger(KeyRingInfo.class); - - 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 - */ - @Nonnull - public static KeyRingInfo evaluateForSignature(@Nonnull PGPKeyRing keyRing, - @Nonnull PGPSignature signature) { - return new KeyRingInfo(keyRing, signature.getCreationTime()); - } - - /** - * Evaluate the key ring right now. - * - * @param keys key ring - */ - public KeyRingInfo(@Nonnull 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(@Nonnull PGPKeyRing keys, - @Nonnull 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(@Nonnull PGPKeyRing keys, - @Nonnull Policy policy, - @Nonnull Date referenceDate) { - this.referenceDate = referenceDate; - this.keys = keys; - this.signatures = new Signatures(keys, this.referenceDate, policy); - this.primaryUserId = findPrimaryUserId(); - this.revocationState = findRevocationState(); - } - - /** - * Return the underlying {@link PGPKeyRing}. - * @return keys - */ - public PGPKeyRing getKeys() { - return keys; - } - - public List getValidSubkeys() { - List subkeys = new ArrayList<>(); - Iterator iterator = getKeys().getPublicKeys(); - while (iterator.hasNext()) { - PGPPublicKey key = iterator.next(); - if (isKeyValidlyBound(key.getKeyID())) { - subkeys.add(key); - } - } - return subkeys; - } - - @Nonnull - 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 - */ - @Nonnull - public PGPPublicKey getPublicKey() { - return keys.getPublicKey(); - } - - /** - * Return the public key with the given fingerprint. - * - * @param fingerprint fingerprint - * @return public key or null - */ - @Nullable - public PGPPublicKey getPublicKey(@Nonnull OpenPgpFingerprint fingerprint) { - return getPublicKey(fingerprint.getKeyId()); - } - - /** - * Return the public key with the given key id. - * - * @param keyId key id - * @return public key or null - */ - @Nullable - public 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 - */ - @Nullable - public static PGPPublicKey getPublicKey(@Nonnull 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 - */ - @Nonnull - 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 - */ - @Nullable - public 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 - */ - @Nullable - public PGPSecretKey getSecretKey(@Nonnull OpenPgpFingerprint fingerprint) { - return getSecretKey(fingerprint.getKeyId()); - } - - /** - * Return the secret key with the given key id. - * - * @param keyId key id - * @return secret key or null - */ - @Nullable - public 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 - */ - @Nonnull - 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 - */ - @Nonnull - public OpenPgpFingerprint getFingerprint() { - return OpenPgpFingerprint.of(getPublicKey()); - } - - @Nullable - public 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 - */ - @Nullable - 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 - */ - @Nonnull - public List getUserIds() { - return KeyRingUtils.getUserIdsIgnoringInvalidUTF8(keys.getPublicKey()); - } - - /** - * Return a list of valid user-ids. - * - * @return valid user-ids - */ - @Nonnull - 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 - */ - @Nonnull - 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(@Nonnull CharSequence userId) { - if (primaryUserId == null) { - // No primary userID? No userID at all! - return false; - } - - if (!userId.equals(primaryUserId)) { - if (!isUserIdBound(primaryUserId)) { - // primary user-id not valid? UserID not valid! - return false; - } - } - return isUserIdBound(userId); - } - - private boolean isUserIdBound(@Nonnull CharSequence userId) { - String userIdString = userId.toString(); - PGPSignature certification = signatures.userIdCertifications.get(userIdString); - PGPSignature revocation = signatures.userIdRevocations.get(userIdString); - - 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 - */ - @Nonnull - 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 - */ - @Nullable - public PGPSignature getLatestDirectKeySelfSignature() { - return signatures.primaryKeySelfSignature; - } - - /** - * Return the latest revocation self-signature on the primary key. - * - * @return revocation or null - */ - @Nullable - public 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 - */ - @Nullable - public PGPSignature getLatestUserIdCertification(@Nonnull CharSequence userId) { - return signatures.userIdCertifications.get(userId.toString()); - } - - /** - * Return the latest user-id revocation signature for the provided user-id. - * - * @param userId user-id - * @return revocation or null - */ - @Nullable - public PGPSignature getUserIdRevocation(@Nonnull CharSequence userId) { - return signatures.userIdRevocations.get(userId.toString()); - } - - /** - * 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 - */ - @Nullable - public 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 - */ - @Nullable - public 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 - */ - @Nonnull - public 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 - */ - @Nonnull - public 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 - */ - @Nonnull - 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. - */ - @Nonnull - public 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 - */ - @Nonnull - public 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; - } - - @Nullable - private 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; - } - - @Nonnull - 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 - */ - @Nullable - public 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 - */ - @Nullable - public 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; - } - - @Nullable - 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 - */ - @Nullable - public 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. - */ - @Nullable - 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 (primaryExpiration == null) { - return latestSubkeyExpirationDate; - } - if (latestSubkeyExpirationDate.before(primaryExpiration)) { - return latestSubkeyExpirationDate; - } - } - return primaryExpiration; - } - - public boolean isHardRevoked(@Nonnull CharSequence userId) { - PGPSignature revocation = signatures.userIdRevocations.get(userId.toString()); - 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 - */ - @Nonnull - public List getEncryptionSubkeys(@Nonnull EncryptionPurpose purpose) { - Date primaryExpiration = getPrimaryKeyExpirationDate(); - if (primaryExpiration != null && primaryExpiration.before(referenceDate)) { - LOGGER.debug("Certificate is expired: Primary key is expired on " + DateUtil.formatUTCDate(primaryExpiration)); - return Collections.emptyList(); - } - - Iterator subkeys = keys.getPublicKeys(); - List encryptionKeys = new ArrayList<>(); - while (subkeys.hasNext()) { - PGPPublicKey subKey = subkeys.next(); - - if (!isKeyValidlyBound(subKey.getKeyID())) { - LOGGER.debug("(Sub?)-Key " + KeyIdUtil.formatKeyId(subKey.getKeyID()) + " is not validly bound."); - continue; - } - - Date subkeyExpiration = getSubkeyExpirationDate(OpenPgpFingerprint.of(subKey)); - if (subkeyExpiration != null && subkeyExpiration.before(referenceDate)) { - LOGGER.debug("(Sub?)-Key " + KeyIdUtil.formatKeyId(subKey.getKeyID()) + " is expired on " + DateUtil.formatUTCDate(subkeyExpiration)); - continue; - } - - if (!subKey.isEncryptionKey()) { - LOGGER.debug("(Sub?)-Key " + KeyIdUtil.formatKeyId(subKey.getKeyID()) + " algorithm is not capable of encryption."); - 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 subkeys that could potentially be used to decrypt a message. - * Contrary to {@link #getEncryptionSubkeys(EncryptionPurpose)}, this method also includes revoked, expired keys, - * as well as keys which do not carry any encryption keyflags. - * Merely keys which use algorithms that cannot be used for encryption at all are excluded. - * That way, decryption of messages produced by faulty implementations can still be decrypted. - * - * @return decryption keys - */ - @Nonnull - public List getDecryptionSubkeys() { - Iterator subkeys = keys.getPublicKeys(); - List decryptionKeys = new ArrayList<>(); - - while (subkeys.hasNext()) { - PGPPublicKey subKey = subkeys.next(); - - // subkeys have been valid at some point - if (subKey.getKeyID() != getKeyId()) { - PGPSignature binding = signatures.subkeyBindings.get(subKey.getKeyID()); - if (binding == null) { - LOGGER.debug("Subkey " + KeyIdUtil.formatKeyId(subKey.getKeyID()) + " was never validly bound."); - continue; - } - } - - // Public-Key algorithm can encrypt - if (!subKey.isEncryptionKey()) { - LOGGER.debug("(Sub?)-Key " + KeyIdUtil.formatKeyId(subKey.getKeyID()) + " is not encryption-capable."); - continue; - } - - decryptionKeys.add(subKey); - } - return decryptionKeys; - } - - /** - * Return a list of all keys which carry the provided key flag in their signature. - * - * @param flag flag - * @return keys with flag - */ - @Nonnull - public List getKeysWithKeyFlag(@Nonnull 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 - */ - @Nonnull - public List getEncryptionSubkeys(@Nullable CharSequence userId, - @Nonnull EncryptionPurpose purpose) { - if (userId != null && !isUserIdValid(userId)) { - throw new KeyException.UnboundUserIdException( - OpenPgpFingerprint.of(keys), - userId.toString(), - getLatestUserIdCertification(userId), - getUserIdRevocation(userId) - ); - } - - return getEncryptionSubkeys(purpose); - } - - /** - * Return a list of all subkeys which can be used to sign data. - * - * @return signing keys - */ - @Nonnull - public 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; - } - - @Nonnull - public Set getPreferredHashAlgorithms() { - return getPreferredHashAlgorithms(getPrimaryUserId()); - } - - @Nonnull - public Set getPreferredHashAlgorithms(@Nullable CharSequence userId) { - return getKeyAccessor(userId, getKeyId()).getPreferredHashAlgorithms(); - } - - @Nonnull - public Set getPreferredHashAlgorithms(long keyId) { - return new KeyAccessor.SubKey(this, new SubkeyIdentifier(keys, keyId)) - .getPreferredHashAlgorithms(); - } - - @Nonnull - public Set getPreferredSymmetricKeyAlgorithms() { - return getPreferredSymmetricKeyAlgorithms(getPrimaryUserId()); - } - - @Nonnull - public Set getPreferredSymmetricKeyAlgorithms(@Nullable CharSequence userId) { - return getKeyAccessor(userId, getKeyId()).getPreferredSymmetricKeyAlgorithms(); - } - - @Nonnull - public Set getPreferredSymmetricKeyAlgorithms(long keyId) { - return new KeyAccessor.SubKey(this, new SubkeyIdentifier(keys, keyId)).getPreferredSymmetricKeyAlgorithms(); - } - - @Nonnull - public Set getPreferredCompressionAlgorithms() { - return getPreferredCompressionAlgorithms(getPrimaryUserId()); - } - - @Nonnull - public Set getPreferredCompressionAlgorithms(@Nullable CharSequence userId) { - return getKeyAccessor(userId, getKeyId()).getPreferredCompressionAlgorithms(); - } - - @Nonnull - 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 CharSequence 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.toString())) { - throw new NoSuchElementException("No user-id '" + userId + "' found on this key."); - } - - if (userId != null) { - return new KeyAccessor.ViaUserId(this, new SubkeyIdentifier(keys, keyID), userId.toString()); - } else { - return new KeyAccessor.ViaKeyId(this, new SubkeyIdentifier(keys, keyID)); - } - } - - 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(@Nonnull PGPKeyRing keyRing, - @Nonnull Date referenceDate, - @Nonnull 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<>(); - - List userIds = KeyRingUtils.getUserIdsIgnoringInvalidUTF8(keyRing.getPublicKey()); - for (String userId : userIds) { - 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 663c6e41..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java +++ /dev/null @@ -1,810 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.modification.secretkeyring; - -import static org.pgpainless.key.util.KeyRingUtils.changePassphrase; -import static org.pgpainless.util.CollectionUtils.concat; - -import java.io.IOException; -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; -import java.util.Collections; -import java.util.Date; -import java.util.List; -import java.util.Map; -import java.util.NoSuchElementException; -import java.util.Set; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -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.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.PGPSignature; -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.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; - -public class SecretKeyRingEditor implements SecretKeyRingEditorInterface { - - private PGPSecretKeyRing secretKeyRing; - private final Date referenceTime; - - public SecretKeyRingEditor(@Nonnull PGPSecretKeyRing secretKeyRing) { - this(secretKeyRing, new Date()); - } - - public SecretKeyRingEditor(@Nonnull PGPSecretKeyRing secretKeyRing, - @Nonnull Date referenceTime) { - this.secretKeyRing = secretKeyRing; - this.referenceTime = referenceTime; - } - - @Nonnull - @Override - public Date getReferenceTime() { - return 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); - 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 - PGPSignature userIdCertification = info.getLatestUserIdCertification(otherUserId); - assert (userIdCertification != null); - - if (userIdCertification.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); - skBindingBuilder.getHashedSubpackets().setSignatureCreationTime(referenceTime); - skBindingBuilder.getHashedSubpackets().setKeyFlags(flags); - - if (subkeyAlgorithm.isSigningCapable()) { - PrimaryKeyBindingSignatureBuilder pkBindingBuilder = new PrimaryKeyBindingSignatureBuilder(secretSubkey, subkeyProtector, hashAlgorithm); - 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 createRevocation(@Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationAttributes revocationAttributes) - throws PGPException { - PGPPublicKey revokeeSubKey = secretKeyRing.getPublicKey(); - PGPSignature revocationCertificate = generateRevocation( - secretKeyRingProtector, revokeeSubKey, callbackFromRevocationAttributes(revocationAttributes)); - return revocationCertificate; - } - - @Override - public PGPSignature createRevocation( - 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 createRevocation( - 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()) { - assert (primaryUserId != null); - PGPSignature userIdSig = reissueNonPrimaryUserId(secretKeyRingProtector, userId, prevUserIdSig); - secretKeyRing = KeyRingUtils.injectCertification(secretKeyRing, primaryUserId, userIdSig); - } - } - - return this; - } - - @Override - public PGPPublicKeyRing createMinimalRevocationCertificate( - @Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationAttributes keyRevocationAttributes) - throws PGPException { - // Check reason - if (keyRevocationAttributes != null && !RevocationAttributes.Reason.isKeyRevocation(keyRevocationAttributes.getReason())) { - throw new IllegalArgumentException("Revocation reason MUST be applicable to a key revocation."); - } - - PGPSignature revocation = createRevocation(secretKeyRingProtector, keyRevocationAttributes); - PGPPublicKey primaryKey = secretKeyRing.getSecretKey().getPublicKey(); - primaryKey = KeyRingUtils.getStrippedDownPublicKey(primaryKey); - primaryKey = PGPPublicKey.addCertification(primaryKey, revocation); - return new PGPPublicKeyRing(Collections.singletonList(primaryKey)); - } - - 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; - } - } - -} 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 dd7ed499..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.java +++ /dev/null @@ -1,652 +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.PGPPublicKeyRing; -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 { - - /** - * Return the editors reference time. - * - * @return reference time - */ - @Nonnull - Date getReferenceTime(); - - /** - * 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 - */ - SecretKeyRingEditorInterface addSubKey( - @Nonnull PGPKeyPair subkey, - @Nullable SelfSignatureSubpackets.Callback bindingSignatureCallback, - @Nonnull SecretKeyRingProtector subkeyProtector, - @Nonnull SecretKeyRingProtector primaryKeyProtector, - @Nonnull KeyFlag keyFlag, - KeyFlag... additionalKeyFlags) - throws PGPException, IOException; - - /** - * 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 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 secretKeyRingProtector protector to unlock the primary key. - * @param keyRevocationAttributes reason for the revocation (key revocation) - * @return minimal revocation certificate - * - * @throws PGPException in case we cannot generate a revocation signature - */ - PGPPublicKeyRing createMinimalRevocationCertificate(@Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationAttributes keyRevocationAttributes) - throws PGPException; - - /** - * 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 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 createRevocation( - @Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationAttributes revocationAttributes) - throws PGPException; - - /** - * 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 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 createRevocation( - long subkeyId, - @Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationAttributes revocationAttributes) - throws PGPException; - - /** - * 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 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 createRevocation( - 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. - * The original key will not be modified by this method. - * - * @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 createRevocation( - OpenPgpFingerprint subkeyFingerprint, - SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationAttributes revocationAttributes) - throws PGPException { - - return createRevocation( - 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 2074e730..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/parsing/KeyRingReader.java +++ /dev/null @@ -1,415 +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 javax.annotation.Nullable; - -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.PGPainless; -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 - */ - @Nullable - 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 - */ - @Nullable - 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 - */ - @Nullable - public PGPKeyRing keyRing(@Nonnull String asciiArmored) - throws IOException { - return keyRing(asciiArmored.getBytes(UTF8)); - } - - @Nullable - public PGPPublicKeyRing publicKeyRing(@Nonnull InputStream inputStream) - throws IOException { - return readPublicKeyRing(inputStream); - } - - @Nullable - public PGPPublicKeyRing publicKeyRing(@Nonnull byte[] bytes) - throws IOException { - return publicKeyRing(new ByteArrayInputStream(bytes)); - } - - @Nullable - public PGPPublicKeyRing publicKeyRing(@Nonnull String asciiArmored) - throws IOException { - return publicKeyRing(asciiArmored.getBytes(UTF8)); - } - - @Nonnull - public PGPPublicKeyRingCollection publicKeyRingCollection(@Nonnull InputStream inputStream) - throws IOException { - return readPublicKeyRingCollection(inputStream); - } - - @Nonnull - public PGPPublicKeyRingCollection publicKeyRingCollection(@Nonnull byte[] bytes) - throws IOException { - return publicKeyRingCollection(new ByteArrayInputStream(bytes)); - } - - @Nonnull - public PGPPublicKeyRingCollection publicKeyRingCollection(@Nonnull String asciiArmored) - throws IOException { - return publicKeyRingCollection(asciiArmored.getBytes(UTF8)); - } - - @Nullable - public PGPSecretKeyRing secretKeyRing(@Nonnull InputStream inputStream) - throws IOException { - return readSecretKeyRing(inputStream); - } - - @Nullable - public PGPSecretKeyRing secretKeyRing(@Nonnull byte[] bytes) - throws IOException { - return secretKeyRing(new ByteArrayInputStream(bytes)); - } - - @Nullable - public PGPSecretKeyRing secretKeyRing(@Nonnull String asciiArmored) - throws IOException { - return secretKeyRing(asciiArmored.getBytes(UTF8)); - } - - @Nonnull - public PGPSecretKeyRingCollection secretKeyRingCollection(@Nonnull InputStream inputStream) - throws IOException { - return readSecretKeyRingCollection(inputStream); - } - - @Nonnull - public PGPSecretKeyRingCollection secretKeyRingCollection(@Nonnull byte[] bytes) - throws IOException { - return secretKeyRingCollection(new ByteArrayInputStream(bytes)); - } - - @Nonnull - public PGPSecretKeyRingCollection secretKeyRingCollection(@Nonnull String asciiArmored) - throws IOException { - return secretKeyRingCollection(asciiArmored.getBytes(UTF8)); - } - - @Nonnull - public PGPKeyRingCollection keyRingCollection(@Nonnull InputStream inputStream, boolean isSilent) - throws IOException, PGPException { - return readKeyRingCollection(inputStream, isSilent); - } - - @Nonnull - public PGPKeyRingCollection keyRingCollection(@Nonnull byte[] bytes, boolean isSilent) - throws IOException, PGPException { - return keyRingCollection(new ByteArrayInputStream(bytes), isSilent); - } - - @Nonnull - 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 - */ - @Nullable - 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 - */ - @Nullable - 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."); - } - - @Nullable - 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 - */ - @Nullable - 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."); - } - - @Nonnull - 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. - * 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 - */ - @Nonnull - 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); - continue; - } - // Parse public keys from secret keys - if (next instanceof PGPSecretKeyRing) { - rings.add(PGPainless.extractCertificate((PGPSecretKeyRing) next)); - continue; - } - 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."); - } - - @Nullable - 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 - */ - @Nullable - 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."); - } - - @Nonnull - 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 - */ - @Nonnull - 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."); - } - - @Nonnull - 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 91de3be4..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java +++ /dev/null @@ -1,589 +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.bcpg.S2K; -import org.bouncycastle.bcpg.SecretKeyPacket; -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.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; -import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; -import org.bouncycastle.util.Strings; -import org.pgpainless.PGPainless; -import org.pgpainless.implementation.ImplementationFactory; -import org.pgpainless.key.protection.SecretKeyRingProtector; -import org.pgpainless.key.protection.UnlockSecretKey; -import org.pgpainless.key.protection.fixes.S2KUsageFix; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public final class KeyRingUtils { - - private KeyRingUtils() { - - } - - private static final Logger LOGGER = LoggerFactory.getLogger(KeyRingUtils.class); - - /** - * 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 - */ - @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()); - } - - /** - * 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 - */ - public static PGPPublicKey getStrippedDownPublicKey(PGPPublicKey bloatedKey) throws PGPException { - return new PGPPublicKey(bloatedKey.getPublicKeyPacket(), ImplementationFactory.getInstance().getKeyFingerprintCalculator()); - } - - public static List getUserIdsIgnoringInvalidUTF8(PGPPublicKey key) { - List userIds = new ArrayList<>(); - Iterator it = key.getRawUserIDs(); - while (it.hasNext()) { - byte[] rawUserId = it.next(); - try { - userIds.add(Strings.fromUTF8ByteArray(rawUserId)); - } catch (IllegalArgumentException e) { - LOGGER.warn("Invalid UTF-8 user-ID encountered: " + new String(rawUserId)); - } - } - return userIds; - } - - public static 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 = KeyRingUtils.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 = KeyRingUtils.reencryptPrivateKey(secretKey, oldProtector, newProtector); - } - secretKeyList.add(secretKey); - } - } - - PGPSecretKeyRing newRing = new PGPSecretKeyRing(secretKeyList); - newRing = s2kUsageFixIfNecessary(newRing, newProtector); - return newRing; - } - - - public 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; - } - - - public static 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; - } -} 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, 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. - * - * @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 6da94bf0..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_128, 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_128, 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 535b9b8e..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/provider/BouncyCastleProviderFactory.java +++ /dev/null @@ -1,23 +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; - -/** - * Implementation of {@link ProviderFactory} using a {@link BouncyCastleProvider}. - */ -public final class BouncyCastleProviderFactory extends ProviderFactory { - - private static final Provider provider = new BouncyCastleProvider(); - - @Override - public Provider _getProvider() { - return provider; - } - -} 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 1859d342..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/provider/ProviderFactory.java +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.provider; - -import java.security.Provider; - -/** - * Allow the use of different {@link Provider} implementations to provide cryptographic primitives by setting - * a {@link ProviderFactory} singleton. - * By default, the class is initialized with a {@link BouncyCastleProviderFactory}. - *
- * To make use of your own custom {@link Provider}, call {@link #setFactory(ProviderFactory)}, passing your - * own custom {@link ProviderFactory} instance. - */ -public abstract class ProviderFactory { - - private static ProviderFactory FACTORY; - - protected abstract Provider _getProvider(); - - protected String _getProviderName() { - return _getProvider().getName(); - } - - 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 663ef003..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureUtils.java +++ /dev/null @@ -1,342 +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 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.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.PGPSignatureList; -import org.bouncycastle.util.encoders.Hex; -import org.bouncycastle.util.io.Streams; -import org.pgpainless.algorithm.SignatureType; -import org.pgpainless.implementation.ImplementationFactory; -import org.pgpainless.key.OpenPgpFingerprint; -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() { - - } - - /** - * 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 - */ - public static List readSignatures(InputStream inputStream, int maxIterations) throws IOException { - 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 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); - } - - public static @Nonnull List getDelegations(PGPPublicKeyRing key) { - List delegations = new ArrayList<>(); - PGPPublicKey primaryKey = key.getPublicKey(); - Iterator signatures = primaryKey.getKeySignatures(); - outerloop: while (signatures.hasNext()) { - PGPSignature signature = signatures.next(); - Iterator subkeys = key.getPublicKeys(); - while (subkeys.hasNext()) { - if (signature.getKeyID() == subkeys.next().getKeyID()) { - continue outerloop; - } - } - delegations.add(signature); - } - - return delegations; - } - - public static @Nonnull List get3rdPartyCertificationsFor(String userId, PGPPublicKeyRing key) { - PGPPublicKey primaryKey = key.getPublicKey(); - List certifications = new ArrayList<>(); - Iterator it = primaryKey.getSignaturesForID(userId); - while (it.hasNext()) { - PGPSignature sig = it.next(); - if (sig.getKeyID() != primaryKey.getKeyID()) { - certifications.add(sig); - } - } - return certifications; - } -} 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 51a14052..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/ThirdPartyDirectKeySignatureBuilder.java +++ /dev/null @@ -1,53 +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(); - 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 f22e057a..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/CertificateValidator.java +++ /dev/null @@ -1,299 +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.key.util.KeyRingUtils; -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 - continue; - // 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) - List userIds = KeyRingUtils.getUserIdsIgnoringInvalidUTF8(primaryKey); - Map> userIdSignatures = new ConcurrentHashMap<>(); - for (String userId : userIds) { - List signaturesOnUserId = new ArrayList<>(); - 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 8ab31b2f..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureValidator.java +++ /dev/null @@ -1,681 +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.Collections; -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); - if (embeddedSignatures == null) { - throw new SignatureValidationException( - "Missing primary key binding signature on signing capable subkey " + - Long.toHexString(subkey.getKeyID()), Collections.emptyMap()); - } - - 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 - */ - public 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 - */ - public 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() || signature.getSignatureType() == PGPSignature.DIRECT_KEY) { - valid = signature.verifyCertification(signee); - } else { - valid = signature.verifyCertification(signer, 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 6d53ad1d..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.java +++ /dev/null @@ -1,703 +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.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.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); - } - - public static boolean isExportable(PGPSignature signature) { - Exportable exportable = getExportableCertification(signature); - return exportable == null || exportable.isExportable(); - } - - /** - * 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); - } - - public static int getTrustDepthOr(PGPSignature signature, int defaultDepth) { - TrustSignature packet = getTrustSignature(signature); - if (packet != null) { - return packet.getDepth(); - } - return defaultDepth; - } - - public static int getTrustAmountOr(PGPSignature signature, int defaultAmount) { - TrustSignature packet = getTrustSignature(signature); - if (packet != null) { - return packet.getTrustAmount(); - } - return defaultAmount; - } - - /** - * Return all regular expression subpackets from the hashed area of the given signature. - * - * @param signature signature - * @return list of regular expressions - */ - public static List getRegularExpressions(PGPSignature signature) { - org.bouncycastle.bcpg.SignatureSubpacket[] subpackets = signature.getHashedSubPackets() - .getSubpackets(SignatureSubpacket.regularExpression.getCode()); - List regularExpressions = new ArrayList<>(subpackets.length); - for (org.bouncycastle.bcpg.SignatureSubpacket subpacket : subpackets) { - regularExpressions.add((RegularExpression) subpacket); - } - return regularExpressions; - } - - - /** - * 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 976f6ab2..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java +++ /dev/null @@ -1,603 +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; -import org.pgpainless.key.util.KeyRingUtils; - -/** - * 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! - List userIds = KeyRingUtils.getUserIdsIgnoringInvalidUTF8(publicKey); - int countIdentities = 0; - String first = null; - String primary = null; - for (String userId : userIds) { - countIdentities++; - // 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); - } - - /** - * 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. - */ - public static void setVersionHeader(@Nonnull ArmoredOutputStream armor, - @Nullable String version) { - if (version == null || version.trim().isEmpty()) { - armor.setHeader(HEADER_VERSION, null); - } else { - armor.setHeader(HEADER_VERSION, version); - } - } - - /** - * 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 77e9c236..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/util/ArmoredInputStreamFactory.java +++ /dev/null @@ -1,43 +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; - -/** - * 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 - */ - @Nonnull - public static ArmoredInputStream get(@Nonnull 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 269f8674..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/util/ArmoredOutputStreamFactory.java +++ /dev/null @@ -1,132 +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; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * 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() { - - } - - private static ArmoredOutputStream.Builder getBuilder() { - ArmoredOutputStream.Builder builder = ArmoredOutputStream.builder(); - builder.clearHeaders(); - if (version != null && !version.isEmpty()) { - builder.setVersion(version); - } - for (String comment : comment) { - builder.addComment(comment); - } - return builder; - } - - /** - * Wrap an {@link OutputStream} inside a preconfigured {@link ArmoredOutputStream}. - * - * @param outputStream inner stream - * @return armored output stream - */ - @Nonnull - public static ArmoredOutputStream get(@Nonnull OutputStream outputStream) { - return getBuilder().build(outputStream); - } - - /** - * 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 - */ - @Nonnull - public static ArmoredOutputStream get(@Nonnull OutputStream outputStream, @Nonnull ProducerOptions options) { - ArmoredOutputStream.Builder builder = getBuilder(); - if (options.isHideArmorHeaders()) { - builder.clearHeaders(); - } - if (options.hasVersion()) { - builder.setVersion(options.getVersion()); - } - if (options.hasComment()) { - builder.setComment(options.getComment()); - } - 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 - */ - public static void setVersionInfo(@Nullable 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(@Nullable 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 f56020d4..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/util/DateUtil.java +++ /dev/null @@ -1,75 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.util; - -import javax.annotation.Nonnull; -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. - @Nonnull - 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 - */ - @Nonnull - public static Date parseUTCDate(@Nonnull String dateString) { - try { - return getParser().parse(dateString); - } catch (ParseException e) { - throw new IllegalArgumentException("Malformed UTC timestamp: " + dateString, e); - } - } - - /** - * Format a date as UTC timestamp. - * - * @param date date - * @return timestamp - */ - @Nonnull - public static String formatUTCDate(Date date) { - return getParser().format(date); - } - - /** - * Floor a date down to seconds precision. - * @param date date - * @return floored date - */ - @Nonnull - public static Date toSecondsPrecision(@Nonnull 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 - */ - @Nonnull - 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 3b342778..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/util/MultiMap.java +++ /dev/null @@ -1,137 +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(); - } - - /** - * Return all values of the {@link MultiMap} in a single {@link LinkedHashSet}. - * - * @return set of all values - */ - public Set flatten() { - LinkedHashSet flattened = new LinkedHashSet<>(); - for (Set items : map.values()) { - flattened.addAll(items); - } - return flattened; - } - - @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/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/java/org/pgpainless/authentication/package-info.java b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/KeyLength.kt similarity index 50% rename from pgpainless-core/src/main/java/org/pgpainless/authentication/package-info.java rename to pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/KeyLength.kt index 495ab1f7..1ff63604 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/authentication/package-info.java +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/KeyLength.kt @@ -2,7 +2,9 @@ // // SPDX-License-Identifier: Apache-2.0 -/** - * Classes and interfaces related to certificate authenticity. - */ -package org.pgpainless.authentication; +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/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java b/pgpainless-core/src/test/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java index b8eab5e4..10cf4b1f 100644 --- a/pgpainless-core/src/test/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java +++ b/pgpainless-core/src/test/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java @@ -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; 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/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 aa1da741..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 @@ -104,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 @@ -137,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 @@ -170,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) @@ -184,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 a0ec7c25..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 @@ -282,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(); @@ -409,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(); @@ -656,7 +656,7 @@ public class OpenPgpMessageInputStreamTest { @Test public void readAfterCloseTest() throws PGPException, IOException { OpenPgpMessageInputStream pgpIn = get(SENC_LIT, ConsumerOptions.get() - .addDecryptionPassphrase(Passphrase.fromPassword(PASSPHRASE))); + .addMessagePassphrase(Passphrase.fromPassword(PASSPHRASE))); Streams.drain(pgpIn); // read all byte[] buf = new byte[1024]; 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 04a98265..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 @@ -184,7 +184,7 @@ 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()); } @@ -200,7 +200,7 @@ 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()); } 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 2821ca89..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,7 +14,6 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; -import javax.annotation.Nonnull; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKey; @@ -63,8 +62,8 @@ public class VerifyWithMissingPublicKeyCallbackTest { .addVerificationCert(unrelatedKeys) .setMissingCertificateCallback(new MissingPublicKeyCallback() { @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; } })); @@ -74,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/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 7d2fa453..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,8 +33,8 @@ 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; @@ -49,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(); @@ -122,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); @@ -150,8 +152,9 @@ public class EncryptionOptionsTest { assertThrows(KeyException.UnacceptableEncryptionKeyException.class, () -> options.addRecipient(publicKeys, new EncryptionOptions.EncryptionKeySelector() { + @NotNull @Override - public List selectEncryptionSubkeys(@Nonnull List encryptionCapableKeys) { + public List selectEncryptionSubkeys(@NotNull List encryptionCapableKeys) { return Collections.emptyList(); } })); @@ -159,7 +162,7 @@ public class EncryptionOptionsTest { assertThrows(KeyException.UnacceptableEncryptionKeyException.class, () -> options.addRecipient(publicKeys, "test@pgpainless.org", new EncryptionOptions.EncryptionKeySelector() { @Override - public List selectEncryptionSubkeys(@Nonnull 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/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 index 5304c32c..28097db6 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/MultiSigningSubkeyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/MultiSigningSubkeyTest.java @@ -21,9 +21,9 @@ 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.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.protection.SecretKeyRingProtector; import org.pgpainless.util.MultiMap; @@ -52,10 +52,10 @@ public class MultiSigningSubkeyTest { @BeforeAll public static void generateKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { signingKey = PGPainless.buildKeyRing() - .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA)) - .addSubkey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.SIGN_DATA)) + .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(XDHSpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)) + .addSubkey(KeySpec.getBuilder(KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)) .addUserId("Alice ") .build(); signingCert = PGPainless.extractCertificate(signingKey); 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 dc9c51f2..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; @@ -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/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/collection/PGPKeyRingCollectionTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/collection/PGPKeyRingCollectionTest.java index 89b0fccf..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,7 +52,7 @@ 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 @@ -63,7 +63,7 @@ public class PGPKeyRingCollectionTest { 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 d62d5bad..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)) @@ -560,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 0caf8b75..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(); 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 1eb086fd..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 @@ -29,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; @@ -47,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) 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/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/RefuseToAddWeakSubkeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/modification/RefuseToAddWeakSubkeyTest.java index e129e4be..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,12 +67,11 @@ 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); 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/provider/ProviderFactoryTest.java b/pgpainless-core/src/test/java/org/pgpainless/provider/ProviderFactoryTest.java index a859008a..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,7 +21,7 @@ public class ProviderFactoryTest { }; @Override - protected Provider _getProvider() { + protected Provider getSecurityProvider() { return provider; } 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/SignatureSubpacketsUtilTest.java b/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureSubpacketsUtilTest.java index 79149dd2..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,7 +140,7 @@ public class SignatureSubpacketsUtilTest { PGPSignatureGenerator generator = getSignatureGenerator(certKey, SignatureType.CASUAL_CERTIFICATION); PGPSignatureSubpacketGenerator hashed = new PGPSignatureSubpacketGenerator(); - hashed.setFeature(true, Feature.toBitmask(Feature.MODIFICATION_DETECTION, Feature.GNUPG_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()); @@ -148,8 +148,8 @@ public class SignatureSubpacketsUtilTest { assertNotNull(featureSet); assertEquals(2, featureSet.size()); assertTrue(featureSet.contains(Feature.MODIFICATION_DETECTION)); - assertTrue(featureSet.contains(Feature.GNUPG_AEAD_ENCRYPTED_DATA)); - assertFalse(featureSet.contains(Feature.GNUPG_VERSION_5_PUBLIC_KEY)); + assertTrue(featureSet.contains(Feature.LIBREPGP_OCB_ENCRYPTED_DATA)); + assertFalse(featureSet.contains(Feature.LIBREPGP_VERSION_5_PUBLIC_KEY)); } @Test 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 bf1cb694..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 @@ -68,7 +68,7 @@ public class ThirdPartyCertificationSignatureBuilderTest { 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 beb87ec5..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 @@ -388,7 +388,7 @@ public class SignatureSubpacketsTest { @Test public void testSetFeatures() { - wrapper.setFeatures(Feature.MODIFICATION_DETECTION, Feature.GNUPG_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.GNUPG_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/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 100% rename from pgpainless-core/src/main/java/org/pgpainless/util/CollectionUtils.java rename to pgpainless-core/src/test/java/org/pgpainless/util/CollectionUtils.java 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 98688a94..164befb1 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/util/MultiMapTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/util/MultiMapTest.java @@ -41,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 @@ -104,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()); @@ -113,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")); @@ -125,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); } @@ -138,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()); @@ -149,9 +179,9 @@ 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()); @@ -160,23 +190,23 @@ public class MultiMapTest { } @Test - public void putAll() { + public void plusAll() { MultiMap map = new MultiMap<>(); - map.put("A", "1"); - map.put("A", "2"); - map.put("B", "1"); + map.plus("A", "1"); + map.plus("A", "2"); + map.plus("B", "1"); MultiMap other = new MultiMap<>(); - other.put("A", "1"); - other.put("B", "2"); - other.put("C", "3"); + other.plus("A", "1"); + other.plus("B", "2"); + other.plus("C", "3"); - map.putAll(other); - assertTrue(map.get("A").contains("1")); - assertTrue(map.get("A").contains("2")); - assertTrue(map.get("B").contains("1")); - assertTrue(map.get("B").contains("2")); - assertTrue(map.get("C").contains("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 @@ -188,9 +218,9 @@ public class MultiMapTest { @Test public void flattenMap() { MultiMap map = new MultiMap<>(); - map.put("A", "1"); - map.put("A", "2"); - map.put("B", "1"); + map.plus("A", "1"); + map.plus("A", "2"); + map.plus("B", "1"); Set expected = new LinkedHashSet<>(); expected.add("1"); 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 55cf0cb2..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: 7](https://img.shields.io/badge/Spec%20Revision-7-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.6.2" + implementation "org.pgpainless:pgpainless-sop:1.7.6" ... } @@ -34,7 +34,7 @@ dependencies { org.pgpainless pgpainless-sop - 1.6.2 + 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 bb4ddaca..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' } @@ -22,7 +22,7 @@ dependencies { 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" @@ -30,7 +30,20 @@ dependencies { 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/ChangeKeyPasswordImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/ChangeKeyPasswordImpl.java deleted file mode 100644 index 95377b12..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/ChangeKeyPasswordImpl.java +++ /dev/null @@ -1,90 +0,0 @@ -// SPDX-FileCopyrightText: 2023 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.bcpg.ArmoredOutputStream; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; -import org.pgpainless.exception.MissingPassphraseException; -import org.pgpainless.key.OpenPgpFingerprint; -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; - -public class ChangeKeyPasswordImpl implements ChangeKeyPassword { - - private final MatchMakingSecretKeyRingProtector oldProtector = new MatchMakingSecretKeyRingProtector(); - private Passphrase newPassphrase = Passphrase.emptyPassphrase(); - private boolean armor = true; - - @Override - public ChangeKeyPassword noArmor() { - armor = false; - return this; - } - - @Override - public ChangeKeyPassword oldKeyPassphrase(String oldPassphrase) { - oldProtector.addPassphrase(Passphrase.fromPassword(oldPassphrase)); - return this; - } - - @Override - public ChangeKeyPassword newKeyPassphrase(String newPassphrase) { - this.newPassphrase = Passphrase.fromPassword(newPassphrase); - return this; - } - - @Override - public Ready keys(InputStream inputStream) throws SOPGPException.KeyIsProtected { - SecretKeyRingProtector newProtector = SecretKeyRingProtector.unlockAnyKeyWith(newPassphrase); - PGPSecretKeyRingCollection secretKeyRingCollection; - try { - secretKeyRingCollection = KeyReader.readSecretKeys(inputStream, true); - } catch (IOException e) { - throw new SOPGPException.BadData(e); - } - - List updatedSecretKeys = new ArrayList<>(); - for (PGPSecretKeyRing secretKeys : secretKeyRingCollection) { - oldProtector.addSecretKey(secretKeys); - try { - PGPSecretKeyRing changed = KeyRingUtils.changePassphrase(null, secretKeys, oldProtector, newProtector); - updatedSecretKeys.add(changed); - } catch (MissingPassphraseException e) { - throw new SOPGPException.KeyIsProtected("Cannot unlock key " + OpenPgpFingerprint.of(secretKeys), e); - } catch (PGPException e) { - if (e.getMessage().contains("Exception decrypting key")) { - throw new SOPGPException.KeyIsProtected("Cannot unlock key " + OpenPgpFingerprint.of(secretKeys), e); - } - throw new RuntimeException("Cannot change passphrase of key " + OpenPgpFingerprint.of(secretKeys), e); - } - } - final PGPSecretKeyRingCollection changedSecretKeyCollection = new PGPSecretKeyRingCollection(updatedSecretKeys); - return new Ready() { - @Override - public void writeTo(OutputStream outputStream) throws IOException { - if (armor) { - ArmoredOutputStream armorOut = ArmoredOutputStreamFactory.get(outputStream); - changedSecretKeyCollection.encode(armorOut); - armorOut.close(); - } else { - changedSecretKeyCollection.encode(outputStream); - } - } - }; - } -} 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 d15713ca..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java +++ /dev/null @@ -1,166 +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(VerificationHelper.mapVerification(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); - } - }; - } -} 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 cdae0215..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.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.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(VerificationHelper.mapVerification(signatureVerification)); - } - - if (!options.getCertificateSource().getExplicitCertificates().isEmpty()) { - if (verificationList.isEmpty()) { - throw new SOPGPException.NoSignature(); - } - } - - return verificationList; - } catch (MalformedOpenPgpMessageException | PGPException e) { - throw new SOPGPException.BadData(e); - } - } -} 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 08c58395..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java +++ /dev/null @@ -1,188 +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.Arrays; -import java.util.HashSet; -import java.util.List; -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.Profile; -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 { - - private static final Profile RFC4880_PROFILE = new Profile("rfc4880", "Follow the packet format of rfc4880"); - - public static final List SUPPORTED_PROFILES = Arrays.asList(RFC4880_PROFILE); - - EncryptionOptions encryptionOptions = EncryptionOptions.get(); - SigningOptions signingOptions = null; - MatchMakingSecretKeyRingProtector protector = new MatchMakingSecretKeyRingProtector(); - private final Set signingKeys = new HashSet<>(); - private String profile = RFC4880_PROFILE.getName(); // TODO: Use in future releases - - 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 = KeyReader.readPublicKeys(cert, true); - encryptionOptions.addRecipients(certificates); - } catch (KeyException.UnacceptableEncryptionKeyException e) { - throw new SOPGPException.CertCannotEncrypt(e.getMessage(), e); - } catch (IOException e) { - throw new SOPGPException.BadData(e); - } - return this; - } - - @Override - public Encrypt profile(String profileName) { - // sanitize profile name to make sure we only accept supported profiles - for (Profile profile : SUPPORTED_PROFILES) { - if (profile.getName().equals(profileName)) { - // profile is supported, return - this.profile = profile.getName(); - return this; - } - } - - // Profile is not supported, throw - throw new SOPGPException.UnsupportedProfile("encrypt", profileName); - } - - @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 b6d5fe73..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java +++ /dev/null @@ -1,146 +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.Arrays; -import java.util.LinkedHashSet; -import java.util.List; -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.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.EdDSACurve; -import org.pgpainless.key.generation.type.rsa.RsaLength; -import org.pgpainless.key.generation.type.xdh.XDHSpec; -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. - */ -public class GenerateKeyImpl implements GenerateKey { - - public static final Profile CURVE25519_PROFILE = new Profile("draft-koch-eddsa-for-openpgp-00", "Generate EdDSA / ECDH keys using Curve25519"); - public static final Profile RSA4096_PROFILE = new Profile("rfc4880", "Generate 4096-bit RSA keys"); - - public static final List SUPPORTED_PROFILES = Arrays.asList(CURVE25519_PROFILE, RSA4096_PROFILE); - - private boolean armor = true; - private boolean signingOnly = false; - private final Set userIds = new LinkedHashSet<>(); - private Passphrase passphrase = Passphrase.emptyPassphrase(); - private String profile = CURVE25519_PROFILE.getName(); - - @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 GenerateKey profile(String profileName) { - // Sanitize the profile name to make sure we support the given profile - for (Profile profile : SUPPORTED_PROFILES) { - if (profile.getName().equals(profileName)) { - this.profile = profileName; - // return if we found the profile - return this; - } - } - - // profile not found, throw - throw new SOPGPException.UnsupportedProfile("generate-key", profileName); - } - - @Override - public GenerateKey signingOnly() { - signingOnly = true; - return this; - } - - @Override - public Ready generate() throws SOPGPException.MissingArg, SOPGPException.UnsupportedAsymmetricAlgo { - try { - final PGPSecretKeyRing key = generateKeyWithProfile(profile, userIds, passphrase, signingOnly); - return new Ready() { - @Override - public void writeTo(OutputStream outputStream) throws IOException { - if (armor) { - ArmoredOutputStream armoredOutputStream = ArmorUtils.toAsciiArmoredStream(key, outputStream); - key.encode(armoredOutputStream); - armoredOutputStream.close(); - } else { - key.encode(outputStream); - } - } - }; - } catch (InvalidAlgorithmParameterException | NoSuchAlgorithmException e) { - throw new SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", e); - } catch (PGPException e) { - throw new RuntimeException(e); - } - } - - private PGPSecretKeyRing generateKeyWithProfile(String profile, Set userIds, Passphrase passphrase, boolean signingOnly) - throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - KeyRingBuilder keyBuilder; - // XDH + EdDSA - if (profile.equals(CURVE25519_PROFILE.getName())) { - keyBuilder = PGPainless.buildKeyRing() - .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER)) - .addSubkey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.SIGN_DATA)); - if (!signingOnly) { - keyBuilder.addSubkey(KeySpec.getBuilder(KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)); - } - } - // RSA 4096 - else if (profile.equals(RSA4096_PROFILE.getName())) { - keyBuilder = PGPainless.buildKeyRing() - .setPrimaryKey(KeySpec.getBuilder(KeyType.RSA(RsaLength._4096), KeyFlag.CERTIFY_OTHER)) - .addSubkey(KeySpec.getBuilder(KeyType.RSA(RsaLength._4096), KeyFlag.SIGN_DATA)); - if (!signingOnly) { - keyBuilder.addSubkey(KeySpec.getBuilder(KeyType.RSA(RsaLength._4096), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)); - } - } - else { - // Missing else-if branch for profile. Oops. - throw new SOPGPException.UnsupportedProfile("generate-key", profile); - } - - for (String userId : userIds) { - keyBuilder.addUserId(userId); - } - if (!passphrase.isEmpty()) { - keyBuilder.setPassphrase(passphrase); - } - return keyBuilder.build(); - } -} 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 aecb891b..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java +++ /dev/null @@ -1,95 +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(VerificationHelper.mapVerification(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); - } - } - }; - } -} 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 a2876a6e..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/KeyReader.java +++ /dev/null @@ -1,62 +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; - } - - if (requireContent && 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) { - String msg = e.getMessage(); - if (msg != null && (msg.startsWith("unknown object in stream:") || msg.startsWith("invalid header encountered"))) { - throw new SOPGPException.BadData(e); - } - throw e; - } - if (requireContent && 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/ListProfilesImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/ListProfilesImpl.java deleted file mode 100644 index 36ef4861..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/ListProfilesImpl.java +++ /dev/null @@ -1,36 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.sop; - -import java.util.List; - -import sop.Profile; -import sop.exception.SOPGPException; -import sop.operation.ListProfiles; - -/** - * Implementation of the
list-profiles
operation using PGPainless. - * - */ -public class ListProfilesImpl implements ListProfiles { - - @Override - public List subcommand(String command) { - if (command == null) { - throw new SOPGPException.UnsupportedProfile("null"); - } - - switch (command) { - case "generate-key": - return GenerateKeyImpl.SUPPORTED_PROFILES; - - case "encrypt": - return EncryptImpl.SUPPORTED_PROFILES; - - default: - throw new SOPGPException.UnsupportedProfile(command); - } - } -} 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 5badf1f1..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/MatchMakingSecretKeyRingProtector.java +++ /dev/null @@ -1,119 +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.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; - -import javax.annotation.Nullable; - -/** - * 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/RevokeKeyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/RevokeKeyImpl.java deleted file mode 100644 index 6d72bf8f..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/RevokeKeyImpl.java +++ /dev/null @@ -1,115 +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.CharacterCodingException; -import java.util.ArrayList; -import java.util.List; - -import org.bouncycastle.bcpg.ArmoredOutputStream; -import org.bouncycastle.bcpg.PublicKeyPacket; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; -import org.bouncycastle.openpgp.PGPSignature; -import org.pgpainless.PGPainless; -import org.pgpainless.exception.WrongPassphraseException; -import org.pgpainless.key.OpenPgpFingerprint; -import org.pgpainless.key.modification.secretkeyring.SecretKeyRingEditorInterface; -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; - -public class RevokeKeyImpl implements RevokeKey { - - private final MatchMakingSecretKeyRingProtector protector = new MatchMakingSecretKeyRingProtector(); - private boolean armor = true; - - public RevokeKey noArmor() { - this.armor = false; - return this; - } - - /** - * Provide the decryption password for the secret key. - * - * @param password password - * @return builder instance - * @throws sop.exception.SOPGPException.UnsupportedOption if the implementation does not support key passwords - * @throws sop.exception.SOPGPException.PasswordNotHumanReadable if the password is not human-readable - */ - public RevokeKey withKeyPassword(byte[] password) - throws SOPGPException.UnsupportedOption, - SOPGPException.PasswordNotHumanReadable { - String string; - try { - string = UTF8Util.decodeUTF8(password); - } catch (CharacterCodingException e) { - throw new SOPGPException.PasswordNotHumanReadable("Cannot UTF8-decode password."); - } - protector.addPassphrase(Passphrase.fromPassword(string)); - return this; - } - - public Ready keys(InputStream keys) throws SOPGPException.BadData { - PGPSecretKeyRingCollection secretKeyRings; - try { - secretKeyRings = KeyReader.readSecretKeys(keys, true); - } catch (IOException e) { - throw new SOPGPException.BadData("Cannot decode secret keys.", e); - } - for (PGPSecretKeyRing secretKeys : secretKeyRings) { - protector.addSecretKey(secretKeys); - } - - final List revocationCertificates = new ArrayList<>(); - for (PGPSecretKeyRing secretKeys : secretKeyRings) { - SecretKeyRingEditorInterface editor = PGPainless.modifyKeyRing(secretKeys); - try { - RevocationAttributes revocationAttributes = RevocationAttributes.createKeyRevocation() - .withReason(RevocationAttributes.Reason.NO_REASON) - .withoutDescription(); - if (secretKeys.getPublicKey().getVersion() == PublicKeyPacket.VERSION_6) { - PGPPublicKeyRing revocation = editor.createMinimalRevocationCertificate(protector, revocationAttributes); - revocationCertificates.add(revocation); - } else { - PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKeys); - PGPSignature revocation = editor.createRevocation(protector, revocationAttributes); - certificate = KeyRingUtils.injectCertification(certificate, revocation); - revocationCertificates.add(certificate); - } - } catch (WrongPassphraseException e) { - throw new SOPGPException.KeyIsProtected("Missing or wrong passphrase for key " + OpenPgpFingerprint.of(secretKeys), e); - } - catch (PGPException e) { - throw new RuntimeException("Cannot generate revocation certificate.", e); - } - } - - return new Ready() { - @Override - public void writeTo(OutputStream outputStream) throws IOException { - PGPPublicKeyRingCollection certificateCollection = new PGPPublicKeyRingCollection(revocationCertificates); - if (armor) { - ArmoredOutputStream out = ArmoredOutputStreamFactory.get(outputStream); - certificateCollection.encode(out); - out.close(); - } else { - certificateCollection.encode(outputStream); - } - } - }; - } -} 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 4009d2a8..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java +++ /dev/null @@ -1,121 +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.ChangeKeyPassword; -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.ListProfiles; -import sop.operation.RevokeKey; -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 ListProfiles listProfiles() { - return new ListProfilesImpl(); - } - - @Override - public RevokeKey revokeKey() { - return new RevokeKeyImpl(); - } - - @Override - public ChangeKeyPassword changeKeyPassword() { - return new ChangeKeyPasswordImpl(); - } - - @Override - public InlineDetach inlineDetach() { - return new InlineDetachImpl(); - } -} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/VerificationHelper.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/VerificationHelper.java deleted file mode 100644 index 126a5e3b..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/VerificationHelper.java +++ /dev/null @@ -1,52 +0,0 @@ -// SPDX-FileCopyrightText: 2023 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 {@link Verification Verifications}. - */ -public class VerificationHelper { - - /** - * Map a {@link SignatureVerification} object to a {@link Verification}. - * - * @param sigVerification signature verification - * @return verification - */ - public static Verification mapVerification(SignatureVerification sigVerification) { - return new Verification( - sigVerification.getSignature().getCreationTime(), - sigVerification.getSigningKey().getSubkeyFingerprint().toString(), - sigVerification.getSigningKey().getPrimaryKeyFingerprint().toString(), - getMode(sigVerification.getSignature()), - null); - } - - /** - * Map an OpenPGP signature type to a {@link SignatureMode} enum. - * Note: This method only maps {@link PGPSignature#BINARY_DOCUMENT} and {@link PGPSignature#CANONICAL_TEXT_DOCUMENT}. - * Other values are mapped to
null
. - * - * @param signature signature - * @return signature mode enum or null - */ - private static SignatureMode getMode(PGPSignature signature) { - - if (signature.getSignatureType() == PGPSignature.BINARY_DOCUMENT) { - return SignatureMode.binary; - } - - if (signature.getSignatureType() == PGPSignature.CANONICAL_TEXT_DOCUMENT) { - return SignatureMode.text; - } - - return null; - } -} 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 488d95b3..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java +++ /dev/null @@ -1,83 +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 int SOP_VERSION = 7; - - @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() { - return "PGPainless " + getVersion(); - } - - @Override - public String getExtendedVersion() { - double bcVersion = new BouncyCastleProvider().getVersion(); - String FORMAT_VERSION = String.format("%02d", SOP_VERSION); - return getName() + " " + getVersion() + "\n" + - "https://codeberg.org/PGPainless/pgpainless/src/branch/master/pgpainless-sop\n" + - "\n" + - "Implementation of the Stateless OpenPGP Protocol Version " + FORMAT_VERSION + "\n" + - "https://datatracker.ietf.org/doc/html/draft-dkg-openpgp-stateless-cli-" + FORMAT_VERSION + "\n" + - "\n" + - "Based on pgpainless-core " + getVersion() + "\n" + - "https://pgpainless.org\n" + - "\n" + - "Using " + String.format(Locale.US, "Bouncy Castle %.2f", bcVersion) + "\n" + - "https://www.bouncycastle.org/java.html"; - } - - @Override - public int getSopSpecRevisionNumber() { - return SOP_VERSION; - } - - @Override - public boolean isSopSpecImplementationIncomplete() { - return false; - } - - @Override - public String getSopSpecImplementationRemarks() { - return null; - } - -} 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 58dfaa62..83778106 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/CarolKeySignEncryptRoundtripTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/CarolKeySignEncryptRoundtripTest.java @@ -11,7 +11,8 @@ import java.io.IOException; import org.junit.jupiter.api.Test; import sop.ByteArrayAndResult; import sop.DecryptionResult; -import sop.Ready; +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()) 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 9238697a..7b2c9131 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/DetachedSignTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/DetachedSignTest.java @@ -51,7 +51,7 @@ public class DetachedSignTest { public void signArmored() throws IOException { byte[] signature = sop.sign() .key(key) - .mode(SignAs.Binary) + .mode(SignAs.binary) .data(data) .toByteArrayAndResult().getBytes(); @@ -95,7 +95,7 @@ public class DetachedSignTest { byte[] signature = sop.sign() .key(key) .noArmor() - .mode(SignAs.Text) + .mode(SignAs.text) .data(data) .toByteArrayAndResult().getBytes(); @@ -142,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 ccfeaf41..aa366ef1 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/EncryptDecryptRoundTripTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/EncryptDecryptRoundTripTest.java @@ -64,6 +64,7 @@ public class EncryptDecryptRoundTripTest { .withCert(aliceCert) .withCert(bobCert) .plaintext(message) + .toByteArrayAndResult() .getBytes(); ByteArrayAndResult bytesAndResult = sop.decrypt() @@ -97,6 +98,7 @@ public class EncryptDecryptRoundTripTest { .withCert(aliceCertNoArmor) .noArmor() .plaintext(message) + .toByteArrayAndResult() .getBytes(); ByteArrayAndResult bytesAndResult = sop.decrypt() @@ -118,6 +120,7 @@ public class EncryptDecryptRoundTripTest { byte[] encrypted = sop.encrypt() .withPassword("passphr4s3") .plaintext(message) + .toByteArrayAndResult() .getBytes(); ByteArrayAndResult bytesAndResult = sop.decrypt() @@ -141,6 +144,7 @@ public class EncryptDecryptRoundTripTest { sop.encrypt() .withPassword("passphr4s3") .plaintext(message) + .toByteArrayAndResult() .getInputStream() ) .toByteArrayAndResult(); @@ -158,6 +162,7 @@ public class EncryptDecryptRoundTripTest { byte[] encrypted = sop.encrypt() .withPassword("passphr4s3 ") .plaintext(message) + .toByteArrayAndResult() .getBytes(); ByteArrayAndResult bytesAndResult = sop.decrypt() @@ -178,6 +183,7 @@ public class EncryptDecryptRoundTripTest { byte[] encrypted = sop.encrypt() .withCert(bobCert) .plaintext(message) + .toByteArrayAndResult() .getBytes(); DecryptionResult result = sop.decrypt() @@ -196,6 +202,7 @@ public class EncryptDecryptRoundTripTest { byte[] encrypted = sop.encrypt() .withCert(bobCert) .plaintext(message) + .toByteArrayAndResult() .getBytes(); assertThrows(SOPGPException.MissingArg.class, () -> sop @@ -266,6 +273,7 @@ public class EncryptDecryptRoundTripTest { byte[] ciphertext = sop.encrypt() .withCert(cert) .plaintext(plaintext) + .toByteArrayAndResult() .getBytes(); byte[] decrypted = sop.decrypt() @@ -308,6 +316,7 @@ public class EncryptDecryptRoundTripTest { .withCert(cert1) .withCert(cert2) .plaintext(plaintext) + .toByteArrayAndResult() .getBytes(); byte[] decrypted = sop.decrypt() @@ -339,6 +348,7 @@ public class EncryptDecryptRoundTripTest { byte[] ciphertext = sop.encrypt() .withCert(cert) .plaintext(plaintext) + .toByteArrayAndResult() .getBytes(); assertThrows(SOPGPException.KeyIsProtected.class, @@ -571,6 +581,7 @@ public class EncryptDecryptRoundTripTest { .profile("rfc4880") .withCert(cert) .plaintext(message) + .toByteArrayAndResult() .getBytes(); ByteArrayAndResult bytesAndResult = sop.decrypt() 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 5894bfa7..521cdfe0 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/GenerateKeyTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/GenerateKeyTest.java @@ -102,8 +102,12 @@ public class GenerateKeyTest { } @Test - public void nullProfile() { - assertThrows(SOPGPException.UnsupportedProfile.class, () -> - sop.generateKey().profile((String) null)); + 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/ListProfilesTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/ListProfilesTest.java index 9158f11e..e15a04cc 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/ListProfilesTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/ListProfilesTest.java @@ -37,9 +37,4 @@ public class ListProfilesTest { sop.listProfiles().subcommand("help")); } - @Test - public void listProfilesOfNullThrows() { - assertThrows(SOPGPException.UnsupportedProfile.class, () -> - sop.listProfiles().subcommand(null)); - } } 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 32d2c2e0..825b5e86 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/VersionTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/VersionTest.java @@ -72,4 +72,11 @@ public class VersionTest { 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 index 78ba6737..baf595d3 100644 --- a/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessChangeKeyPasswordTest.java +++ b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessChangeKeyPasswordTest.java @@ -13,8 +13,8 @@ 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.pgpainless.util.Passphrase; import sop.SOP; import sop.testsuite.TestData; @@ -34,9 +34,9 @@ public class PGPainlessChangeKeyPasswordTest extends ChangeKeyPasswordTest { @MethodSource("provideInstances") public void changePasswordOfKeyWithSeparateSubkeyPasswords(SOP sop) throws IOException, PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() - .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER)) - .addSubkey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.SIGN_DATA)) - .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.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(); @@ -75,6 +75,7 @@ public class PGPainlessChangeKeyPasswordTest extends ChangeKeyPasswordTest { .withKeyPassword(p4) .withCert(cert) .plaintext(TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8)) + .toByteArrayAndResult() .getBytes(); byte[] plaintext = sop.decrypt() .verifyWithCert(cert) 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 73c5547c..bc2515a4 100644 --- a/version.gradle +++ b/version.gradle @@ -4,16 +4,15 @@ allprojects { ext { - shortVersion = '1.6.3' + shortVersion = '1.7.7' isSnapshot = true - pgpainlessMinAndroidSdk = 10 - javaSourceCompatibility = 1.8 - bouncyCastleVersion = '1.76' + 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 = '7.0.0' + sopJavaVersion = '10.1.1' } }