diff --git a/.editorconfig b/.editorconfig index a9ff980b..9bf812d1 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,51 +1,513 @@ -# 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-FileCopyrightText: 2021 Paul Schaub # -# It includes editor-specific config options for IntelliJ IDEA. -# -# If any option is wrong, PR are welcome +# SPDX-License-Identifier: CC0-1.0 -[{*.kt,*.kts}] -indent_style = space -insert_final_newline = true -max_line_length = 100 +[*] +charset = utf-8 +end_of_line = lf indent_size = 4 -ij_continuation_indent_size = 4 # was 8 -ij_java_names_count_to_use_import_on_demand = 9999 +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}] 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 = true -ij_kotlin_allow_trailing_comma_on_call_site = true -ij_kotlin_assignment_wrap = normal +ij_kotlin_allow_trailing_comma = false +ij_kotlin_allow_trailing_comma_on_call_site = false +ij_kotlin_assignment_wrap = off 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 = true +ij_kotlin_call_parameters_new_line_after_left_paren = false ij_kotlin_call_parameters_right_paren_on_new_line = false -ij_kotlin_call_parameters_wrap = on_every_item +ij_kotlin_call_parameters_wrap = off 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 = 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_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_else_on_new_line = false ij_kotlin_enum_constants_wrap = off -ij_kotlin_extends_list_wrap = normal +ij_kotlin_extends_list_wrap = off 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 @@ -57,13 +519,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 = 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_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_parameter_annotation_wrap = off ij_kotlin_space_after_comma = true ij_kotlin_space_after_extend_colon = true @@ -90,5 +552,72 @@ 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 = 1 +ij_kotlin_wrap_expression_body_functions = 0 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 deleted file mode 100644 index 447355cc..00000000 --- a/.git-blame-ignore-revs +++ /dev/null @@ -1,2 +0,0 @@ -# Ignore initial spotlessApply using ktfmt -51e9bfc67f19e16a69790a8d92bd6b1c86a76a5f diff --git a/.github/ISSUE_TEMPLATE/cli-application.md b/.github/ISSUE_TEMPLATE/cli-application.md deleted file mode 100644 index 68f35b74..00000000 --- a/.github/ISSUE_TEMPLATE/cli-application.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -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 deleted file mode 100644 index 74f5f666..00000000 --- a/.github/ISSUE_TEMPLATE/library.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -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 d45b16a3..b8726876 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-kotlin' ] + language: [ 'java' ] # 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@v3 + uses: github/codeql-action/init@v1 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@v3 + uses: github/codeql-action/autobuild@v1 # â„šī¸ 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@v3 + uses: github/codeql-action/analyze@v1 diff --git a/.github/workflows/gradle_push.yml b/.github/workflows/gradle_push.yml index 8c03a399..e4624726 100644 --- a/.github/workflows/gradle_push.yml +++ b/.github/workflows/gradle_push.yml @@ -28,13 +28,9 @@ jobs: with: java-version: '11' distribution: 'temurin' - - name: Build and Check - uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1 - with: - arguments: check jacocoRootReport - - name: Coveralls + - name: Build, Check and Coverage uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1 env: COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} with: - arguments: coveralls + arguments: check jacocoRootReport coveralls \ No newline at end of file diff --git a/.reuse/dep5 b/.reuse/dep5 new file mode 100644 index 00000000..96efa937 --- /dev/null +++ b/.reuse/dep5 @@ -0,0 +1,71 @@ +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 13c93c07..6fc3d29a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,72 +5,6 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog -## 1.7.7-SNAPSHOT -- Bump `bcpg-jdk8on` to `1.81` -- Bump `bcprov-jdk18on` to `1.81` - -## 1.7.6 -- Fix `RevocationSignatureBuilder` properly calculating third-party signatures of type `KeyRevocation` (delegation revocations) -- Enable support for native images -- Re-enable shadow plugin and build fat-jar - -## 1.7.5 -- Actually attempt to fix Kotlin desugaring. -- Bump javaSourceCompatibility and javaTargetCompatibility to 11 -- Bump gradle-wrapper to 8.8 - -## 1.7.4 -- Fix proper Kotlin desugaring for Java 8 - -## 1.7.3 -- Bump `bcpg-jdk8on` to `1.80` -- Bump `bcprov-jdk18on` to `1.80` -- Add dependency on `bcutil-jdk18on` as a workaround -- Ignore unknown type signatures on certificates -- Fix typo on signature creation bounds check (thanks @elduffy) -- Fix superfluous newline added in CRLF encoding (thanks @bjansen) -- Bump `sop-java` to `1.10.0` - - SOP inline-sign: Do not apply compression - -## 1.7.2 -- Fix bug in `KeyRingInfo.lastModified` (thanks to @Jerbell, @sosnovsky for reporting) -- Bump `sop-java` to `10.0.3` - - allow multiple arguments `--with-key-password` in `revoke-key` command - - Properly pass `--old-key-password` and `--new-key-password` options as indirect arguments in `change-key-password` command - -## 1.7.1 -- Bump `sop-java` to `10.0.2` -- Downgrade `logback-core` and `logback-classic` to `1.2.13` (fix CLI spam) - -## 1.7.0 -- Bump `bcpg-jdk8on` to `1.78.1` -- Bump `bcprov-jdk18on` to `1.78.1` -- Bump `logback-core` and `logback-classic` to `1.4.14` -- `pgpainless-core` - - Rewrote most of the codebase in Kotlin - - Removed `OpenPgpMetadata` (`decryptionStream.getResult()`) in favor of `MessageMetadata` (`decryptionStream.getMetadata()`) - - Removed support for generating EC keys over non-standard curve `secp256k1` - - Properly feed EOS tokens to the pushdown automaton when reaching the end of stream (thanks @iNPUTmice) - - Do not choke on unknown signature subpackets (thanks @Jerbell) - - Prevent timing issues resulting in subkey binding signatures predating the subkey (@thanks Jerbell) - - Rename LibrePGP-related `Feature` enums: - - `GNUPG_AEAD_ENCRYPTED_DATA` -> `LIBREPGP_OCB_ENCRYPTED_DATA` - - `GNUPG_VERSION_5_PUBLIC_KEY` -> `LIBREPGP_VERSION_5_PUBLIC_KEY` - - Properly reject signatures by non-signing primary keys - - Add `EncryptionBuilder.discardOutput()` (useful for detached signing) - - Remove support for generation of keys over non-standard `secp256k1` curve - - Add base support for padding packets - - Do not choke on LibrePGP OED packets - - Supersede `addPassphrase()`/`addDecryptionPassphrase()` methods with more clear `addMessagePassphrase()` -- `pgpainless-sop`, `pgpainless-cli` - - Bump `sop-java` to `10.0.1`, implementing [SOP Spec Revision 10](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-10.html) - - Change API of `sop.encrypt` to return a `ReadyWithResult` to expose the session key - - `decrypt --verify-with`: Fix to not throw `NoSignature` exception (exit code 3) if `VERIFICATIONS` is empty - - Separate signature verification operations into `SOPV` interface - - Add `version --sopv` option - - Throw `BadData` error when passing KEYS where CERTS are expected. - - `armor`: Remove `--label` option - ## 1.6.8 - Bump `sop-java` to `7.0.2` - SOP `change-key-password`: Fix reading password from indirect parameter instead of erroneously passing filename (fixes #453) @@ -94,7 +28,7 @@ SPDX-License-Identifier: CC0-1.0 - 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) +- Prevent timing issues resulting in subkey binding signatures predating the subkey (@thanks Jerbell) ## 1.6.3 - Bump `sop-java` to `7.0.1` @@ -122,10 +56,6 @@ 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 @@ -202,10 +132,6 @@ 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 @@ -279,10 +205,6 @@ 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 e305e43a..f3a14377 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/results/green)](https://tests.sequoia-pgp.org/) +[![Interoperability Test-Suite](https://badgen.net/badge/Sequoia%20Test%20Suite/%231/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 - .addMessagePassphrase(Passphrase.fromPassword("password123")) + .addPassphrase(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.7.6' + implementation 'org.pgpainless:pgpainless-core:1.6.8' } ``` @@ -222,6 +222,9 @@ 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 deleted file mode 100644 index 66b5e867..00000000 --- a/REUSE.toml +++ /dev/null @@ -1,118 +0,0 @@ -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 a05079d3..d668040b 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -12,14 +12,12 @@ 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 | 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: | | +| Version | Supported | +|---------|--------------------| +| 1.5.X | :white_check_mark: | +| 1.4.X | :white_check_mark: | +| 1.3.X | :white_check_mark: | +| < 1.3.X | :x: | ## Reporting a Vulnerability @@ -34,4 +32,4 @@ Valid security issues will be fixed ASAP. PGPainless has received a security audit by [cure53.de](https://cure53.de) in late 2021. The [penetrationj test and audit](https://cure53.de/pentest-report_pgpainless.pdf) covered PGPainless release candidate 1.0.0-rc6. -Security fixes for discovered flaws were deployed before the final 1.0.0 release. \ No newline at end of file +Security fixes for discovered flaws were deployed before the final 1.0.0 release. diff --git a/build.gradle b/build.gradle index 93100f4d..d04b6345 100644 --- a/build.gradle +++ b/build.gradle @@ -18,8 +18,7 @@ buildscript { } plugins { - id 'org.jetbrains.kotlin.jvm' version "1.8.10" - id 'com.diffplug.spotless' version '6.22.0' apply false + id 'ru.vyarus.animalsniffer' version '1.5.3' } apply from: 'version.gradle' @@ -30,32 +29,41 @@ allprojects { apply plugin: 'eclipse' apply plugin: 'jacoco' apply plugin: 'checkstyle' - apply plugin: 'kotlin' - apply plugin: 'com.diffplug.spotless' + + compileJava { + options.release = 8 + } // 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.25.0' - } - - spotless { - kotlin { - ktfmt().dropboxStyle() - } + toolVersion = '10.12.1' } group 'org.pgpainless' description = "Simple to use OpenPGP API for Java based on Bouncycastle" version = shortVersion + sourceCompatibility = javaSourceCompatibility + repositories { mavenCentral() mavenLocal() @@ -70,17 +78,6 @@ 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() @@ -114,7 +111,7 @@ allprojects { sourceDirectories.setFrom(project.files(sourceSets.main.allSource.srcDirs)) classDirectories.setFrom(project.files(sourceSets.main.output)) reports { - xml.required = true + xml.enabled true } } @@ -132,15 +129,15 @@ subprojects { apply plugin: 'signing' task sourcesJar(type: Jar, dependsOn: classes) { - archiveClassifier = 'sources' + classifier = 'sources' from sourceSets.main.allSource } task javadocJar(type: Jar, dependsOn: javadoc) { - archiveClassifier = 'javadoc' + classifier = 'javadoc' from javadoc.destinationDir } task testsJar(type: Jar, dependsOn: testClasses) { - archiveClassifier = 'tests' + classifier = 'tests' from sourceSets.test.output } @@ -237,7 +234,7 @@ task jacocoRootReport(type: JacocoReport) { classDirectories.setFrom(files(subprojects.sourceSets.main.output)) executionData.setFrom(files(subprojects.jacocoTestReport.executionData)) reports { - xml.required = true + xml.enabled true xml.destination file("${buildDir}/reports/jacoco/test/jacocoTestReport.xml") } // We could remove the following setOnlyIf line, but then @@ -248,6 +245,10 @@ 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') @@ -261,6 +262,18 @@ 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. * @@ -270,13 +283,34 @@ task mavenCentralChecksums() { description 'Fetch and display checksums for artifacts published to Maven Central' String ver = project.hasProperty('release') ? release : shortVersion doLast { - for (Project p : rootProject.subprojects) { - String url = "https://repo1.maven.org/maven2/org/pgpainless/${p.name}/${ver}/${p.name}-${ver}.jar.sha256" - Process fetch = "curl -f $url".execute() - if (fetch.waitFor() == 0) { - print fetch.text.trim() - println " ${p.name}/build/libs/${p.name}-${ver}.jar" - } + 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" } } } diff --git a/docs/source/pgpainless-cli/usage.md b/docs/source/pgpainless-cli/usage.md index 15a87846..4bc1d166 100644 --- a/docs/source/pgpainless-cli/usage.md +++ b/docs/source/pgpainless-cli/usage.md @@ -82,26 +82,23 @@ Stateless OpenPGP Protocol Usage: pgpainless-cli [--stacktrace] [COMMAND] Options: - --stacktrace Print stacktrace + --stacktrace Print Stacktrace Commands: - version Display version information about the tool - list-profiles Emit a list of profiles supported by the identified - subcommand - generate-key Generate a secret key - change-key-password Update the password of a key - revoke-key Generate revocation certificates - extract-cert Extract a public key certificate from a secret key - sign Create a detached message signature - verify Verify a detached signature - encrypt Encrypt a message from standard input - decrypt Decrypt a message - inline-detach Split signatures from a clearsigned message - inline-sign Create an inline-signed message - inline-verify Verify an inline-signed message - armor Add ASCII Armor to standard input - dearmor Remove ASCII Armor from standard input - help Display usage information for the specified subcommand + 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 Exit Codes: 0 Successful program execution @@ -123,9 +120,6 @@ 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 1816983f..abb86358 100644 --- a/docs/source/pgpainless-core/quickstart.md +++ b/docs/source/pgpainless-core/quickstart.md @@ -50,15 +50,9 @@ 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 55958ee4..8b8281ac 100644 --- a/docs/source/pgpainless-sop/quickstart.md +++ b/docs/source/pgpainless-sop/quickstart.md @@ -180,6 +180,14 @@ 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 0d184210..8049c684 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-8.8-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/pgpainless-cli/build.gradle b/pgpainless-cli/build.gradle index e4c3f060..3d9a6a09 100644 --- a/pgpainless-cli/build.gradle +++ b/pgpainless-cli/build.gradle @@ -4,13 +4,27 @@ plugins { id 'application' - id 'org.graalvm.buildtools.native' version '0.10.6' - id 'com.gradleup.shadow' version '8.3.6' + id "com.github.johnrengelman.shadow" version "6.1.0" +} +def generatedVersionDir = "${buildDir}/generated-version" + +sourceSets { + main { + output.dir(generatedVersionDir, builtBy: 'generateVersionProperties') + } } -graalvmNative { - toolchainDetection = true +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()) + } } +processResources.dependsOn generateVersionProperties dependencies { @@ -18,12 +32,13 @@ 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 "org.slf4j:slf4j-nop:$slf4jVersion" + implementation "ch.qos.logback:logback-classic:$logbackVersion" implementation(project(":pgpainless-sop")) implementation "org.pgpainless:sop-java-picocli:$sopJavaVersion" @@ -37,6 +52,22 @@ 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 @@ -46,3 +77,5 @@ 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 0d948dfd..98e98857 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-armor.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-armor.1 @@ -30,11 +30,16 @@ pgpainless\-cli\-armor \- Add ASCII Armor to standard input .SH "SYNOPSIS" .sp -\fBpgpainless\-cli armor\fP [\fB\-\-stacktrace\fP] +\fBpgpainless\-cli armor\fP [\fB\-\-stacktrace\fP] [\fB\-\-label\fP=\fI{auto|sig|key|cert|message}\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 eb843e18..9a3c80a7 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\-\-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\-\-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\-\-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\-\-verifications\-out\fP=\fIVERIFICATIONS\fP, \fB\-\-verify\-not\-after\fP=\fIDATE\fP +\fB\-\-verify\-not\-after\fP=\fIDATE\fP .RS 4 ISO\-8601 formatted UTC date (e.g. \(aq2020\-11\-23T16:35Z) .sp @@ -69,6 +69,11 @@ 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 3cf7aefc..29f1d1f1 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\-\-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...] +[\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...] .SH "DESCRIPTION" .SH "OPTIONS" @@ -53,7 +53,7 @@ ASCII armor the output Profile identifier to switch between profiles .RE .sp -\fB\-\-session\-key\-out\fP=\fISESSIONKEY\fP, \fB\-\-sign\-with\fP=\fIKEY\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 c552bbe0..87db756a 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 | \fB\-\-sopv\fP] +\fBpgpainless\-cli version\fP [\fB\-\-stacktrace\fP] [\fB\-\-extended\fP | \fB\-\-backend\fP | \fB\-\-pgpainless\-cli\-spec\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\-\-sopv\fP, \fB\-\-stacktrace\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 730af68d..37b4fdf3 100755 --- a/pgpainless-cli/rewriteManPages.sh +++ b/pgpainless-cli/rewriteManPages.sh @@ -13,14 +13,12 @@ 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 7625dd17..938bf1aa 100644 --- a/pgpainless-cli/src/main/java/org/pgpainless/cli/PGPainlessCLI.java +++ b/pgpainless-cli/src/main/java/org/pgpainless/cli/PGPainlessCLI.java @@ -14,10 +14,6 @@ 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 deleted file mode 100644 index 32960f8c..00000000 --- a/pgpainless-cli/src/main/resources/META-INF/native-image/jni-config.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] \ 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 deleted file mode 100644 index 84789507..00000000 --- a/pgpainless-cli/src/main/resources/META-INF/native-image/predefined-classes-config.json +++ /dev/null @@ -1,7 +0,0 @@ -[ - { - "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 deleted file mode 100644 index 32960f8c..00000000 --- a/pgpainless-cli/src/main/resources/META-INF/native-image/proxy-config.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] \ 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 deleted file mode 100644 index 63bdf5f3..00000000 --- a/pgpainless-cli/src/main/resources/META-INF/native-image/reflect-config.json +++ /dev/null @@ -1,891 +0,0 @@ -[ -{ - "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 deleted file mode 100644 index 3c66a520..00000000 --- a/pgpainless-cli/src/main/resources/META-INF/native-image/resource-config.json +++ /dev/null @@ -1,93 +0,0 @@ -{ - "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 deleted file mode 100644 index 3f06d9a8..00000000 --- a/pgpainless-cli/src/main/resources/META-INF/native-image/serialization-config.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "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 8451d6a4..559589ef 100644 --- a/pgpainless-cli/src/main/resources/logback.xml +++ b/pgpainless-cli/src/main/resources/logback.xml @@ -5,5 +5,22 @@ 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 b1ca143a..07c9bf68 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/ExitCodeTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/ExitCodeTest.java @@ -4,35 +4,28 @@ 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; -import java.io.IOException; +public class ExitCodeTest { -import static org.junit.jupiter.api.Assertions.assertEquals; - -public class ExitCodeTest extends CLITest { - - public ExitCodeTest() { - super(LoggerFactory.getLogger(ExitCodeTest.class)); + @Test + @ExpectSystemExitWithStatus(SOPGPException.UnsupportedSubcommand.EXIT_CODE) + public void testUnknownCommand_69() { + PGPainlessCLI.main(new String[] {"generate-kex"}); } @Test - public void testUnknownCommand_69() throws IOException { - assertEquals(SOPGPException.UnsupportedSubcommand.EXIT_CODE, - executeCommand("unsupported-subcommand")); + @ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE) + public void testCommandWithUnknownOption_37() { + PGPainlessCLI.main(new String[] {"generate-key", "-k", "\"k is unknown\""}); } @Test - 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")); + @FailOnSystemExit + public void successfulExecutionDoesNotTerminateJVM() { + PGPainlessCLI.main(new String[] {"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 ddb3c6dd..afd5ded4 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,6 +16,7 @@ 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 { @@ -88,6 +89,15 @@ 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 f8d56bc3..1d89aeeb 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,6 +14,7 @@ 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; @@ -23,8 +24,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_legacy.EdDSALegacyCurve; -import org.pgpainless.key.generation.type.xdh_legacy.XDHLegacySpec; +import org.pgpainless.key.generation.type.eddsa.EdDSACurve; +import org.pgpainless.key.generation.type.xdh.XDHSpec; import org.slf4j.LoggerFactory; import sop.exception.SOPGPException; @@ -82,6 +83,7 @@ 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"); @@ -302,7 +304,7 @@ public class RoundTripEncryptDecryptCmdTest extends CLITest { InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() .addUserId("No Crypt ") - .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA)) .build(); PGPPublicKeyRing cert = PGPainless.extractCertificate(secretKeys); @@ -321,9 +323,9 @@ public class RoundTripEncryptDecryptCmdTest extends CLITest { throws IOException, PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() .addUserId("Cannot Sign ") - .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER)) + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER)) .addSubkey(KeySpec.getBuilder( - KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)) + KeyType.XDH(XDHSpec._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 057cec98..0676f213 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,10 +138,6 @@ 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); @@ -157,7 +153,7 @@ public class RoundTripInlineSignInlineVerifyCmdTest extends CLITest { String cleartextSigned = ciphertextOut.toString(); assertTrue(cleartextSigned.startsWith("-----BEGIN PGP SIGNED MESSAGE-----\n" + "Hash: ")); - assertTrue(cleartextSigned.contains(MESSAGE_CRLF)); + assertTrue(cleartextSigned.contains(MESSAGE)); assertTrue(cleartextSigned.contains("\n-----BEGIN PGP SIGNATURE-----\n")); assertTrue(cleartextSigned.endsWith("-----END PGP SIGNATURE-----\n")); } @@ -207,7 +203,7 @@ public class RoundTripInlineSignInlineVerifyCmdTest extends CLITest { "--verifications-out", verifications.getAbsolutePath(), cert.getAbsolutePath())); - assertEquals(MESSAGE_CRLF, plaintextOut.toString()); + assertEquals(MESSAGE, 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 cf350e66..82fda430 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,49 +4,104 @@ package org.pgpainless.cli.commands; -import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintStream; import java.nio.charset.StandardCharsets; +import com.ginsberg.junit.exit.FailOnSystemExit; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import org.slf4j.LoggerFactory; +import org.pgpainless.cli.PGPainlessCLI; +import org.pgpainless.cli.TestUtils; -public class RoundTripInlineSignVerifyCmdTest extends CLITest { +public class RoundTripInlineSignVerifyCmdTest { + private static File tempDir; + private static PrintStream originalSout; - public RoundTripInlineSignVerifyCmdTest() { - super(LoggerFactory.getLogger(RoundTripInlineSignVerifyCmdTest.class)); + @BeforeAll + public static void prepare() throws IOException { + tempDir = TestUtils.createTempDirectory(); } @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 - File password = writeFile("password", "sw0rdf1sh"); + FileOutputStream passwordOut = new FileOutputStream(passwordFile); + passwordOut.write("sw0rdf1sh".getBytes(StandardCharsets.UTF_8)); + passwordOut.close(); // generate key - File sigmundKey = pipeStdoutToFile("sigmund.key"); - assertSuccess(executeCommand("generate-key", "--with-key-password=" + password.getAbsolutePath(), - "Sigmund Freud ")); + OutputStream sigmundKeyOut = new FileOutputStream(sigmundKeyFile); + System.setOut(new PrintStream(sigmundKeyOut)); + PGPainlessCLI.execute("generate-key", + "--with-key-password=" + passwordFile.getAbsolutePath(), + "Sigmund Freud "); + sigmundKeyOut.close(); // extract cert - File sigmundCert = pipeStdoutToFile("sigmund.cert"); - pipeFileToStdin(sigmundKey); - assertSuccess(executeCommand("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(); // sign message - pipeBytesToStdin("Hello, World!\n".getBytes(StandardCharsets.UTF_8)); - File signedMsg = pipeStdoutToFile("signed.asc"); - assertSuccess(executeCommand("inline-sign", "--with-key-password=" + password.getAbsolutePath(), - sigmundKey.getAbsolutePath())); + 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(); - // verify message - File verifyFile = nonExistentFile("verify.txt"); - pipeFileToStdin(signedMsg); - assertSuccess(executeCommand("inline-verify", "--verifications-out", verifyFile.getAbsolutePath(), - sigmundCert.getAbsolutePath())); + File verifyFile = new File(tempDir, "verify.txt"); - String verifications = readStringFromFile(verifyFile); - assertFalse(verifications.trim().isEmpty()); + 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 } } 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 9dcb3aca..0ff83144 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_legacy.EdDSALegacyCurve; -import org.pgpainless.key.generation.type.xdh_legacy.XDHLegacySpec; +import org.pgpainless.key.generation.type.eddsa.EdDSACurve; +import org.pgpainless.key.generation.type.xdh.XDHSpec; 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_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER)) - .addSubkey(KeySpec.getBuilder(KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)) + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER)) + .addSubkey(KeySpec.getBuilder(KeyType.XDH(XDHSpec._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 d6065b82..affe621e 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,18 +4,28 @@ 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.commands.CLITest; -import org.slf4j.LoggerFactory; +import org.pgpainless.cli.PGPainlessCLI; +import org.pgpainless.cli.TestUtils; import sop.exception.SOPGPException; -public class SignUsingPublicKeyBehaviorTest extends CLITest { +public class SignUsingPublicKeyBehaviorTest { public static final String KEY_THAT_IS_A_CERT = "" + "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + @@ -79,24 +89,61 @@ public class SignUsingPublicKeyBehaviorTest extends CLITest { "=oJQ2\n" + "-----END PGP PUBLIC KEY BLOCK-----"; - public SignUsingPublicKeyBehaviorTest() { - super(LoggerFactory.getLogger(SignUsingPublicKeyBehaviorTest.class)); + + private static File tempDir; + private static PrintStream originalSout; + + @BeforeAll + public static void prepare() throws IOException { + tempDir = TestUtils.createTempDirectory(); } @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 = writeFile("alice.key", KEY_THAT_IS_A_CERT); + 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(); // Write test data to disc - File dataFile = writeFile("data", "If privacy is outlawed, only outlaws will have privacy.\n"); + 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(); // Sign test data - File sigFile = pipeStdoutToFile("sig.asc"); - pipeFileToStdin(dataFile); - assertEquals(SOPGPException.KeyCannotSign.EXIT_CODE, - executeCommand("sign", "--armor", aliceKeyFile.getAbsolutePath())); + 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()}); - assertTrue(readStringFromFile(sigFile).trim().isEmpty()); + System.setIn(originalIn); + } + + @AfterAll + public static void after() { + System.setOut(originalSout); + // CHECKSTYLE:OFF + System.out.println(tempDir.getAbsolutePath()); + // CHECKSTYLE:ON } } diff --git a/pgpainless-core/build.gradle b/pgpainless-core/build.gradle index 64d538d5..bab6ecf1 100644 --- a/pgpainless-core/build.gradle +++ b/pgpainless-core/build.gradle @@ -22,7 +22,6 @@ 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 diff --git a/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java b/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java new file mode 100644 index 00000000..3da54177 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java @@ -0,0 +1,238 @@ +// 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 new file mode 100644 index 00000000..a5885005 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/AEADAlgorithm.java @@ -0,0 +1,100 @@ +// 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 new file mode 100644 index 00000000..e155e367 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/AlgorithmSuite.java @@ -0,0 +1,63 @@ +// 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 new file mode 100644 index 00000000..f5c8ec7e --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/CertificationType.java @@ -0,0 +1,46 @@ +// 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 new file mode 100644 index 00000000..a2e78c5f --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/CompressionAlgorithm.java @@ -0,0 +1,79 @@ +// 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 new file mode 100644 index 00000000..4dbc58da --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/DocumentSignatureType.java @@ -0,0 +1,35 @@ +// 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 new file mode 100644 index 00000000..5eda30c0 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/EncryptionPurpose.java @@ -0,0 +1,22 @@ +// 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 new file mode 100644 index 00000000..d9aa9b90 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/Feature.java @@ -0,0 +1,152 @@ +// 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 new file mode 100644 index 00000000..0b9368bb --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/HashAlgorithm.java @@ -0,0 +1,118 @@ +// 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 new file mode 100644 index 00000000..92205fd9 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/KeyFlag.java @@ -0,0 +1,121 @@ +// 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 new file mode 100644 index 00000000..41e3fb08 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/OpenPgpPacket.java @@ -0,0 +1,71 @@ +// 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 new file mode 100644 index 00000000..c7599db9 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/PublicKeyAlgorithm.java @@ -0,0 +1,163 @@ +// 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 new file mode 100644 index 00000000..8e4a60d3 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/RevocationState.java @@ -0,0 +1,131 @@ +// 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 new file mode 100644 index 00000000..d1757255 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/RevocationStateType.java @@ -0,0 +1,23 @@ +// 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 new file mode 100644 index 00000000..9429f0c6 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/SignatureSubpacket.java @@ -0,0 +1,462 @@ +// 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 new file mode 100644 index 00000000..c2f02989 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/SignatureType.java @@ -0,0 +1,225 @@ +// 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 new file mode 100644 index 00000000..b0617bbb --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/StreamEncoding.java @@ -0,0 +1,101 @@ +// 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 new file mode 100644 index 00000000..e04f21a5 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/SymmetricKeyAlgorithm.java @@ -0,0 +1,150 @@ +// 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 new file mode 100644 index 00000000..26e66e9c --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/Trustworthiness.java @@ -0,0 +1,188 @@ +// 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 new file mode 100644 index 00000000..18fe53f9 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/negotiation/HashAlgorithmNegotiator.java @@ -0,0 +1,70 @@ +// 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 new file mode 100644 index 00000000..de8ced24 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/negotiation/SymmetricKeyAlgorithmNegotiator.java @@ -0,0 +1,105 @@ +// 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 new file mode 100644 index 00000000..3969d8df --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/negotiation/package-info.java @@ -0,0 +1,8 @@ +// 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 new file mode 100644 index 00000000..c2f5f5ff --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/package-info.java @@ -0,0 +1,8 @@ +// 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 new file mode 100644 index 00000000..ac059646 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/authentication/CertificateAuthenticity.java @@ -0,0 +1,139 @@ +// 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 new file mode 100644 index 00000000..36bf9e5f --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/authentication/CertificateAuthority.java @@ -0,0 +1,71 @@ +// 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/kotlin/org/pgpainless/key/generation/type/KeyLength.kt b/pgpainless-core/src/main/java/org/pgpainless/authentication/package-info.java similarity index 50% rename from pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/KeyLength.kt rename to pgpainless-core/src/main/java/org/pgpainless/authentication/package-info.java index 1ff63604..495ab1f7 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/KeyLength.kt +++ b/pgpainless-core/src/main/java/org/pgpainless/authentication/package-info.java @@ -2,9 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package org.pgpainless.key.generation.type - -interface KeyLength { - - val length: Int -} +/** + * Classes and interfaces related to certificate authenticity. + */ +package org.pgpainless.authentication; diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/CachingBcPublicKeyDataDecryptorFactory.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/CachingBcPublicKeyDataDecryptorFactory.java new file mode 100644 index 00000000..9196cc37 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/CachingBcPublicKeyDataDecryptorFactory.java @@ -0,0 +1,94 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPrivateKey; +import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; +import org.bouncycastle.openpgp.operator.bc.BcPublicKeyDataDecryptorFactory; +import org.bouncycastle.util.encoders.Base64; +import org.bouncycastle.util.encoders.Hex; +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/pgpainless/decryption_verification/ConsumerOptions.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java new file mode 100644 index 00000000..d0d9230b --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java @@ -0,0 +1,508 @@ +// 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 new file mode 100644 index 00000000..91902cc7 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactory.java @@ -0,0 +1,27 @@ +// 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 new file mode 100644 index 00000000..96b4ad60 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilder.java @@ -0,0 +1,42 @@ +// 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 new file mode 100644 index 00000000..07db42f0 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilderInterface.java @@ -0,0 +1,36 @@ +// 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 new file mode 100644 index 00000000..28642bbf --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStream.java @@ -0,0 +1,33 @@ +// 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 new file mode 100644 index 00000000..fc88ef33 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java @@ -0,0 +1,105 @@ +// 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 new file mode 100644 index 00000000..37dcfca4 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/IntegrityProtectedInputStream.java @@ -0,0 +1,60 @@ +// 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 new file mode 100644 index 00000000..3dda0f5d --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageInspector.java @@ -0,0 +1,146 @@ +// 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 new file mode 100644 index 00000000..041a5437 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java @@ -0,0 +1,858 @@ +// 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 new file mode 100644 index 00000000..ed6a9c63 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MissingKeyPassphraseStrategy.java @@ -0,0 +1,21 @@ +// 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 new file mode 100644 index 00000000..9b6f4a1e --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MissingPublicKeyCallback.java @@ -0,0 +1,33 @@ +// 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 3522f509..ff020a7b 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,7 +32,6 @@ 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; @@ -209,8 +208,8 @@ public class OpenPgpInputStream extends BufferedInputStream { } try { - SignatureType.requireFromCode(sigType); - } catch (NoSuchElementException e) { + SignatureType.valueOf(sigType); + } catch (IllegalArgumentException e) { return; } @@ -237,8 +236,8 @@ public class OpenPgpInputStream extends BufferedInputStream { if (opsVersion == 3) { int opsSigType = bcpgIn.read(); try { - SignatureType.requireFromCode(opsSigType); - } catch (NoSuchElementException e) { + SignatureType.valueOf(opsSigType); + } catch (IllegalArgumentException 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 new file mode 100644 index 00000000..dd8cf0e2 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -0,0 +1,1140 @@ +// 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.next(InputSymbol.EndOfSequence); + 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 new file mode 100644 index 00000000..e2d5f1ca --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMetadata.java @@ -0,0 +1,380 @@ +// 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 new file mode 100644 index 00000000..4a810eb9 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/SignatureVerification.java @@ -0,0 +1,106 @@ +// 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 new file mode 100644 index 00000000..725c6f6e --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java @@ -0,0 +1,159 @@ +// 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 new file mode 100644 index 00000000..cd0f6b35 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/ClearsignedMessageUtil.java @@ -0,0 +1,169 @@ +// 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 new file mode 100644 index 00000000..62433a2a --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/InMemoryMultiPassStrategy.java @@ -0,0 +1,35 @@ +// 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 new file mode 100644 index 00000000..5aa9f548 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/MultiPassStrategy.java @@ -0,0 +1,70 @@ +// 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 new file mode 100644 index 00000000..6c8d03ca --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/WriteToFileMultiPassStrategy.java @@ -0,0 +1,54 @@ +// 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 new file mode 100644 index 00000000..3123338f --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/package-info.java @@ -0,0 +1,8 @@ +// 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 new file mode 100644 index 00000000..854c3305 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/InputSymbol.java @@ -0,0 +1,45 @@ +// 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 new file mode 100644 index 00000000..9d20e0a8 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.java @@ -0,0 +1,142 @@ +// 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 new file mode 100644 index 00000000..7d7cf973 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java @@ -0,0 +1,156 @@ +// 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 new file mode 100644 index 00000000..120458e5 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/StackSymbol.java @@ -0,0 +1,20 @@ +// 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 new file mode 100644 index 00000000..9dee9af1 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/State.java @@ -0,0 +1,16 @@ +// 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 new file mode 100644 index 00000000..2f3d0a57 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Syntax.java @@ -0,0 +1,33 @@ +// 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 new file mode 100644 index 00000000..ab0db5ef --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Transition.java @@ -0,0 +1,48 @@ +// 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 new file mode 100644 index 00000000..4df6af5a --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/package-info.java @@ -0,0 +1,8 @@ +// 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 new file mode 100644 index 00000000..54c6df9e --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/BcHashContextSigner.java @@ -0,0 +1,66 @@ +// 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 new file mode 100644 index 00000000..5cdf9e36 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/BcPGPHashContextContentSignerBuilder.java @@ -0,0 +1,174 @@ +// 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 new file mode 100644 index 00000000..4cab8be3 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/CRLFGeneratorStream.java @@ -0,0 +1,55 @@ +// 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 new file mode 100644 index 00000000..9490551b --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionBuilder.java @@ -0,0 +1,77 @@ +// 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 new file mode 100644 index 00000000..c705c0b1 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionBuilderInterface.java @@ -0,0 +1,38 @@ +// 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 new file mode 100644 index 00000000..bb937a1a --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java @@ -0,0 +1,473 @@ +// 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 new file mode 100644 index 00000000..4112092c --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionResult.java @@ -0,0 +1,209 @@ +// 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 new file mode 100644 index 00000000..7af5a7b3 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java @@ -0,0 +1,313 @@ +// 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 new file mode 100644 index 00000000..7b8529fe --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/PGPHashContextContentSignerBuilder.java @@ -0,0 +1,83 @@ +// 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 new file mode 100644 index 00000000..00fbb10a --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/ProducerOptions.java @@ -0,0 +1,355 @@ +// 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 new file mode 100644 index 00000000..7a96f2e1 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SignatureGenerationStream.java @@ -0,0 +1,65 @@ +// 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 new file mode 100644 index 00000000..34d4bbcf --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java @@ -0,0 +1,634 @@ +// 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 new file mode 100644 index 00000000..4a2f1f39 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/package-info.java @@ -0,0 +1,8 @@ +// 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 65d27390..66c5d2cf 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/exception/KeyException.java +++ b/pgpainless-core/src/main/java/org/pgpainless/exception/KeyException.java @@ -67,13 +67,6 @@ 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 2141ec5c..b5b8941d 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/exception/SignatureValidationException.java +++ b/pgpainless-core/src/main/java/org/pgpainless/exception/SignatureValidationException.java @@ -28,14 +28,7 @@ public class SignatureValidationException extends PGPException { StringBuilder sb = new StringBuilder(); sb.append(rejections.size()).append(" rejected signatures:\n"); for (PGPSignature signature : rejections.keySet()) { - 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(' ') + sb.append(SignatureType.valueOf(signature.getSignatureType())).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 new file mode 100644 index 00000000..cbc320e6 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/implementation/BcImplementationFactory.java @@ -0,0 +1,154 @@ +// 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 new file mode 100644 index 00000000..90d1330f --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/implementation/ImplementationFactory.java @@ -0,0 +1,119 @@ +// 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 new file mode 100644 index 00000000..10760de2 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/implementation/JceImplementationFactory.java @@ -0,0 +1,141 @@ +// 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 new file mode 100644 index 00000000..3ce87531 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/implementation/package-info.java @@ -0,0 +1,8 @@ +// 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 new file mode 100644 index 00000000..13804061 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpFingerprint.java @@ -0,0 +1,189 @@ +// 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 new file mode 100644 index 00000000..13a79201 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpV4Fingerprint.java @@ -0,0 +1,149 @@ +// 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 new file mode 100644 index 00000000..a0a3d1f4 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpV5Fingerprint.java @@ -0,0 +1,58 @@ +// 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 new file mode 100644 index 00000000..79cc1715 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpV6Fingerprint.java @@ -0,0 +1,58 @@ +// 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 new file mode 100644 index 00000000..8bfd7f7c --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/SubkeyIdentifier.java @@ -0,0 +1,146 @@ +// 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 new file mode 100644 index 00000000..11f18058 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/_64DigitFingerprint.java @@ -0,0 +1,119 @@ +// 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 new file mode 100644 index 00000000..9340d93d --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/certification/CertifyCertificate.java @@ -0,0 +1,294 @@ +// 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 new file mode 100644 index 00000000..db2f4857 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/certification/package-info.java @@ -0,0 +1,8 @@ +// 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 new file mode 100644 index 00000000..6cf7102d --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/collection/PGPKeyRingCollection.java @@ -0,0 +1,114 @@ +// 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 new file mode 100644 index 00000000..b2f5b153 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/collection/package-info.java @@ -0,0 +1,8 @@ +// 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 new file mode 100644 index 00000000..346837c7 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingBuilder.java @@ -0,0 +1,330 @@ +// 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 { + Date keyCreationDate = spec.getKeyCreationDate(); + if (keyCreationDate == null) { + keyCreationDate = new Date(); + } + return generateKeyPair(spec, keyCreationDate); + } + + public static PGPKeyPair generateKeyPair(KeySpec spec, Date keyCreationDate) + throws NoSuchAlgorithmException, PGPException, + InvalidAlgorithmParameterException { + KeyType type = spec.getKeyType(); + KeyPairGenerator certKeyGenerator = KeyPairGenerator.getInstance(type.getName(), + ProviderFactory.getProvider()); + certKeyGenerator.initialize(type.getAlgorithmSpec()); + + // Create raw Key Pair + KeyPair keyPair = certKeyGenerator.generateKeyPair(); + + // Form PGP key pair + PGPKeyPair pgpKeyPair = ImplementationFactory.getInstance() + .getPGPKeyPair(type.getAlgorithm(), keyPair, 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 new file mode 100644 index 00000000..ecff123b --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingBuilderInterface.java @@ -0,0 +1,45 @@ +// 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 new file mode 100644 index 00000000..6966232b --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingTemplates.java @@ -0,0 +1,255 @@ +// 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 new file mode 100644 index 00000000..63645edd --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeySpec.java @@ -0,0 +1,62 @@ +// 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 new file mode 100644 index 00000000..559dd3ce --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeySpecBuilder.java @@ -0,0 +1,92 @@ +// 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 new file mode 100644 index 00000000..4a99bc8d --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeySpecBuilderInterface.java @@ -0,0 +1,26 @@ +// 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 new file mode 100644 index 00000000..96728227 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/generation/package-info.java @@ -0,0 +1,8 @@ +// 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 new file mode 100644 index 00000000..1cadfef8 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/KeyLength.java @@ -0,0 +1,10 @@ +// 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 new file mode 100644 index 00000000..191a22f7 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/KeyType.java @@ -0,0 +1,118 @@ +// 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 new file mode 100644 index 00000000..2372896e --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/ecc/EllipticCurve.java @@ -0,0 +1,42 @@ +// 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 new file mode 100644 index 00000000..bb7e3f3c --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/ecc/ecdh/ECDH.java @@ -0,0 +1,46 @@ +// 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 new file mode 100644 index 00000000..b1f2c882 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/ecc/ecdh/package-info.java @@ -0,0 +1,8 @@ +// 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 new file mode 100644 index 00000000..87301655 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/ecc/ecdsa/ECDSA.java @@ -0,0 +1,48 @@ +// 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 new file mode 100644 index 00000000..9b8ca577 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/ecc/ecdsa/package-info.java @@ -0,0 +1,8 @@ +// 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 new file mode 100644 index 00000000..d55a487a --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/ecc/package-info.java @@ -0,0 +1,8 @@ +// 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 new file mode 100644 index 00000000..2db57f57 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/eddsa/EdDSA.java @@ -0,0 +1,50 @@ +// 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 new file mode 100644 index 00000000..4d5aed1c --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/eddsa/EdDSACurve.java @@ -0,0 +1,28 @@ +// 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 new file mode 100644 index 00000000..cba16f54 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/eddsa/package-info.java @@ -0,0 +1,8 @@ +// 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 new file mode 100644 index 00000000..23c33d3d --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/elgamal/ElGamal.java @@ -0,0 +1,52 @@ +// 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 new file mode 100644 index 00000000..17e79131 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/elgamal/ElGamalLength.java @@ -0,0 +1,81 @@ +// 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 new file mode 100644 index 00000000..19bc0214 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/elgamal/package-info.java @@ -0,0 +1,8 @@ +// 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 new file mode 100644 index 00000000..bf048484 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/package-info.java @@ -0,0 +1,8 @@ +// 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 new file mode 100644 index 00000000..3cf717b2 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/rsa/RSA.java @@ -0,0 +1,48 @@ +// 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 new file mode 100644 index 00000000..74951f0d --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/rsa/RsaLength.java @@ -0,0 +1,29 @@ +// 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 new file mode 100644 index 00000000..2a2a0120 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/rsa/package-info.java @@ -0,0 +1,8 @@ +// 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 new file mode 100644 index 00000000..4e589677 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/xdh/XDH.java @@ -0,0 +1,45 @@ +// 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 new file mode 100644 index 00000000..ccbd2038 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/xdh/XDHSpec.java @@ -0,0 +1,34 @@ +// 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 new file mode 100644 index 00000000..96af405a --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/xdh/package-info.java @@ -0,0 +1,8 @@ +// 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 new file mode 100644 index 00000000..8ab8a9c4 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyAccessor.java @@ -0,0 +1,167 @@ +// 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 new file mode 100644 index 00000000..f455608e --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyInfo.java @@ -0,0 +1,139 @@ +// 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 new file mode 100644 index 00000000..77277622 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java @@ -0,0 +1,1314 @@ +// 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 new file mode 100644 index 00000000..9f33dd40 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/info/package-info.java @@ -0,0 +1,8 @@ +// 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 new file mode 100644 index 00000000..9fa73e88 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/modification/package-info.java @@ -0,0 +1,8 @@ +// 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 new file mode 100644 index 00000000..a8c569a2 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java @@ -0,0 +1,882 @@ +// 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.KeyIdUtil; +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); + hashedSubpackets.setSignatureCreationTime(referenceTime); + } + }; + + 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, referenceTime); + + 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 SecretKeyRingEditorInterface setExpirationDateOfSubkey(@Nullable Date expiration, long keyId, @Nonnull SecretKeyRingProtector secretKeyRingProtector) throws PGPException { + // is primary key + if (keyId == secretKeyRing.getPublicKey().getKeyID()) { + return setExpirationDate(expiration, secretKeyRingProtector); + } + + // is subkey + PGPPublicKey subkey = secretKeyRing.getPublicKey(keyId); + if (subkey != null) { + PGPSignature prevBinding = PGPainless.inspectKeyRing(secretKeyRing).getCurrentSubkeyBindingSignature(keyId); + PGPSignature bindingSig = reissueSubkeyBindingSignature(subkey, expiration, secretKeyRingProtector, prevBinding); + secretKeyRing = KeyRingUtils.injectCertification(secretKeyRing, subkey, bindingSig); + } else { + throw new NoSuchElementException("No subkey with ID " + KeyIdUtil.formatKeyId(keyId) + " found."); + } + 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 reissueSubkeyBindingSignature( + PGPPublicKey subkey, + Date expiration, + SecretKeyRingProtector secretKeyRingProtector, + PGPSignature prevSubkeyBindingSignature) + throws PGPException { + if (subkey == null) { + throw new IllegalArgumentException("Subkey MUST NOT be null."); + } + if (prevSubkeyBindingSignature == null) { + throw new IllegalArgumentException("Previous subkey binding signature for " + + KeyIdUtil.formatKeyId(subkey.getKeyID()) + " MUST NOT be null."); + } + PGPPublicKey primaryKey = secretKeyRing.getPublicKey(); + PGPSecretKey secretPrimaryKey = secretKeyRing.getSecretKey(); + PGPSecretKey secretSubkey = secretKeyRing.getSecretKey(subkey.getKeyID()); + + if (secretPrimaryKey == null) { + throw new NoSuchElementException("Secret Key Ring does not contain secret-key component for the primary key."); + } + + SubkeyBindingSignatureBuilder builder = new SubkeyBindingSignatureBuilder( + secretPrimaryKey, secretKeyRingProtector, prevSubkeyBindingSignature); + SelfSignatureSubpackets subpackets = builder.getHashedSubpackets(); + if (referenceTime != null) { + subpackets.setSignatureCreationTime(referenceTime); + } + // Set expiration + subpackets.setKeyExpirationTime(subkey, expiration); + subpackets.setSignatureExpirationTime(null); // avoid copying sig exp time + + List previousKeyFlags = SignatureSubpacketsUtil.parseKeyFlags(prevSubkeyBindingSignature); + if (previousKeyFlags != null && previousKeyFlags.contains(KeyFlag.SIGN_DATA)) { + if (secretSubkey == null) { + throw new NoSuchElementException("Secret keyring does not contain secret-key component for subkey " + + KeyIdUtil.formatKeyId(subkey.getKeyID())); + } + + // Create new embedded back-sig + subpackets.clearEmbeddedSignatures(); + try { + subpackets.addEmbeddedSignature( + new PrimaryKeyBindingSignatureBuilder(secretSubkey, secretKeyRingProtector) + .build(primaryKey)); + } catch (IOException e) { + throw new PGPException("Cannot add embedded primary-key back signature.", e); + } + } + return builder.build(subkey); + } + + 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 new file mode 100644 index 00000000..f746934e --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.java @@ -0,0 +1,669 @@ +// 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; + + /** + * 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 secretKeyRingProtector 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 + */ + SecretKeyRingEditorInterface setExpirationDateOfSubkey( + @Nullable Date expiration, + long keyId, + @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 new file mode 100644 index 00000000..6b3eb3b3 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/package-info.java @@ -0,0 +1,8 @@ +// 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 new file mode 100644 index 00000000..2074e730 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/parsing/KeyRingReader.java @@ -0,0 +1,415 @@ +// 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 new file mode 100644 index 00000000..50030499 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/parsing/package-info.java @@ -0,0 +1,8 @@ +// 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 new file mode 100644 index 00000000..1a31d0e8 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/protection/BaseSecretKeyRingProtector.java @@ -0,0 +1,70 @@ +// 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 new file mode 100644 index 00000000..8cdb3efb --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/protection/CachingSecretKeyRingProtector.java @@ -0,0 +1,222 @@ +// 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 new file mode 100644 index 00000000..a93534ab --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/protection/KeyRingProtectionSettings.java @@ -0,0 +1,99 @@ +// 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 new file mode 100644 index 00000000..c5745068 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/protection/PasswordBasedSecretKeyRingProtector.java @@ -0,0 +1,78 @@ +// 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 new file mode 100644 index 00000000..d7ed5c85 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/protection/SecretKeyRingProtector.java @@ -0,0 +1,168 @@ +// 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 new file mode 100644 index 00000000..78c849ab --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/protection/UnlockSecretKey.java @@ -0,0 +1,66 @@ +// 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 new file mode 100644 index 00000000..26b5c596 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/protection/UnprotectedKeysProtector.java @@ -0,0 +1,33 @@ +// 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 new file mode 100644 index 00000000..24fe533a --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/protection/fixes/S2KUsageFix.java @@ -0,0 +1,92 @@ +// 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 new file mode 100644 index 00000000..06c299b1 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/protection/fixes/package-info.java @@ -0,0 +1,8 @@ +// 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 new file mode 100644 index 00000000..b936025f --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/protection/package-info.java @@ -0,0 +1,8 @@ +// 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 new file mode 100644 index 00000000..859758b1 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/MapBasedPassphraseProvider.java @@ -0,0 +1,42 @@ +// 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 new file mode 100644 index 00000000..59cb39ce --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/SecretKeyPassphraseProvider.java @@ -0,0 +1,40 @@ +// 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 new file mode 100644 index 00000000..b439ef56 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/SolitaryPassphraseProvider.java @@ -0,0 +1,33 @@ +// 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 new file mode 100644 index 00000000..e70ad81f --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/package-info.java @@ -0,0 +1,8 @@ +// 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 new file mode 100644 index 00000000..7ebac8fd --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyIdUtil.java @@ -0,0 +1,37 @@ +// 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 new file mode 100644 index 00000000..91de3be4 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java @@ -0,0 +1,589 @@ +// 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 f7a78404..e97a2d7a 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,11 +34,7 @@ public final class OpenPgpKeyAttributeUtil { continue; } - SignatureType signatureType = SignatureType.fromCode(signature.getSignatureType()); - if (signatureType == null) { - // unknown signature type - continue; - } + SignatureType signatureType = SignatureType.valueOf(signature.getSignatureType()); if (signatureType == SignatureType.POSITIVE_CERTIFICATION || signatureType == SignatureType.GENERIC_CERTIFICATION) { int[] hashAlgos = signature.getHashedSubPackets().getPreferredHashAlgorithms(); @@ -75,8 +71,8 @@ public final class OpenPgpKeyAttributeUtil { continue; } - SignatureType signatureType = SignatureType.fromCode(signature.getSignatureType()); - if (signatureType == null || signatureType != SignatureType.POSITIVE_CERTIFICATION + SignatureType signatureType = SignatureType.valueOf(signature.getSignatureType()); + if (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 new file mode 100644 index 00000000..344f063b --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/PublicKeyParameterValidationUtil.java @@ -0,0 +1,291 @@ +// 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 new file mode 100644 index 00000000..6da94bf0 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/policy/Policy.java @@ -0,0 +1,733 @@ +// 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 new file mode 100644 index 00000000..dd248151 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/policy/package-info.java @@ -0,0 +1,8 @@ +// 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 new file mode 100644 index 00000000..535b9b8e --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/provider/BouncyCastleProviderFactory.java @@ -0,0 +1,23 @@ +// 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 new file mode 100644 index 00000000..1859d342 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/provider/ProviderFactory.java @@ -0,0 +1,46 @@ +// 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 new file mode 100644 index 00000000..b5faa3f8 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/provider/package-info.java @@ -0,0 +1,8 @@ +// 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 new file mode 100644 index 00000000..663ef003 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureUtils.java @@ -0,0 +1,342 @@ +// 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 new file mode 100644 index 00000000..1079f92f --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/AbstractSignatureBuilder.java @@ -0,0 +1,140 @@ +// 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 new file mode 100644 index 00000000..cf99f8d6 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/DirectKeySelfSignatureBuilder.java @@ -0,0 +1,57 @@ +// 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 new file mode 100644 index 00000000..156e739f --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/PrimaryKeyBindingSignatureBuilder.java @@ -0,0 +1,59 @@ +// 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 new file mode 100644 index 00000000..3c2dcab9 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/RevocationSignatureBuilder.java @@ -0,0 +1,71 @@ +// 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 new file mode 100644 index 00000000..e6bf94c3 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/SelfSignatureBuilder.java @@ -0,0 +1,74 @@ +// 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 new file mode 100644 index 00000000..c15e219e --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/SubkeyBindingSignatureBuilder.java @@ -0,0 +1,72 @@ +// 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 new file mode 100644 index 00000000..d75e269d --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/ThirdPartyCertificationSignatureBuilder.java @@ -0,0 +1,119 @@ +// 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 new file mode 100644 index 00000000..51a14052 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/ThirdPartyDirectKeySignatureBuilder.java @@ -0,0 +1,53 @@ +// 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 new file mode 100644 index 00000000..7ca512bf --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/UniversalSignatureBuilder.java @@ -0,0 +1,55 @@ +// 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 new file mode 100644 index 00000000..c82eb0ec --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/package-info.java @@ -0,0 +1,8 @@ +// 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 new file mode 100644 index 00000000..f22e057a --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/CertificateValidator.java @@ -0,0 +1,299 @@ +// 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 new file mode 100644 index 00000000..7a6a5b10 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/OnePassSignatureCheck.java @@ -0,0 +1,72 @@ +// 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/kotlin/org/pgpainless/signature/consumer/README.md b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/README.md similarity index 100% rename from pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/README.md rename to pgpainless-core/src/main/java/org/pgpainless/signature/consumer/README.md 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 new file mode 100644 index 00000000..bc9f1f0b --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureCheck.java @@ -0,0 +1,73 @@ +// 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 new file mode 100644 index 00000000..7996e33b --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureCreationDateComparator.java @@ -0,0 +1,53 @@ +// 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 new file mode 100644 index 00000000..e6f2f755 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignaturePicker.java @@ -0,0 +1,395 @@ +// 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 new file mode 100644 index 00000000..8ab31b2f --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureValidator.java @@ -0,0 +1,681 @@ +// 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 new file mode 100644 index 00000000..94ebd5b2 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureValidityComparator.java @@ -0,0 +1,53 @@ +// 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 new file mode 100644 index 00000000..c4565197 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureVerifier.java @@ -0,0 +1,486 @@ +// 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 new file mode 100644 index 00000000..e8e53285 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/package-info.java @@ -0,0 +1,8 @@ +// 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 new file mode 100644 index 00000000..624a2e24 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/package-info.java @@ -0,0 +1,8 @@ +// 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 new file mode 100644 index 00000000..28e99b9e --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/BaseSignatureSubpackets.java @@ -0,0 +1,131 @@ +// 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 new file mode 100644 index 00000000..24614882 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/CertificationSubpackets.java @@ -0,0 +1,12 @@ +// 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 new file mode 100644 index 00000000..358437dc --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/RevocationSignatureSubpackets.java @@ -0,0 +1,26 @@ +// 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 new file mode 100644 index 00000000..92bf02b3 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SelfSignatureSubpackets.java @@ -0,0 +1,98 @@ +// 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, @Nullable Date keyExpirationTime); + + SelfSignatureSubpackets setKeyExpirationTime(@Nonnull Date keyCreationTime, @Nullable Date keyExpirationTime); + + SelfSignatureSubpackets setKeyExpirationTime(boolean isCritical, @Nonnull Date keyCreationTime, @Nullable 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 new file mode 100644 index 00000000..11650ea3 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketCallback.java @@ -0,0 +1,26 @@ +// 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 new file mode 100644 index 00000000..be65dd7e --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpackets.java @@ -0,0 +1,739 @@ +// 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, @Nullable Date keyExpirationTime) { + return setKeyExpirationTime(key.getCreationTime(), keyExpirationTime); + } + + @Override + public SignatureSubpackets setKeyExpirationTime(@Nonnull Date keyCreationTime, @Nullable Date keyExpirationTime) { + return setKeyExpirationTime(true, keyCreationTime, keyExpirationTime); + } + + @Override + public SignatureSubpackets setKeyExpirationTime(boolean isCritical, @Nonnull Date keyCreationTime, @Nullable Date keyExpirationTime) { + if (keyExpirationTime != null) { + return setKeyExpirationTime(isCritical, (keyExpirationTime.getTime() / 1000) - (keyCreationTime.getTime() / 1000)); + } else { + return setKeyExpirationTime(isCritical, 0L); + } + } + + @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 new file mode 100644 index 00000000..7f105a85 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsHelper.java @@ -0,0 +1,202 @@ +// 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.fromCode(subpacket.getType()); + if (type == null) { + // Unknown subpacket, ignore and just add to the residuals + subpackets.addResidualSubpacket(subpacket); + continue; + } + + 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 new file mode 100644 index 00000000..6d53ad1d --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.java @@ -0,0 +1,703 @@ +// 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 new file mode 100644 index 00000000..09dfd6a2 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/package-info.java @@ -0,0 +1,8 @@ +// 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 new file mode 100644 index 00000000..976f6ab2 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java @@ -0,0 +1,603 @@ +// 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 new file mode 100644 index 00000000..77e9c236 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/util/ArmoredInputStreamFactory.java @@ -0,0 +1,43 @@ +// 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 new file mode 100644 index 00000000..269f8674 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/util/ArmoredOutputStreamFactory.java @@ -0,0 +1,132 @@ +// 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 new file mode 100644 index 00000000..2c43339b --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/util/CRCingArmoredInputStreamWrapper.java @@ -0,0 +1,153 @@ +// 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/test/java/org/pgpainless/util/CollectionUtils.java b/pgpainless-core/src/main/java/org/pgpainless/util/CollectionUtils.java similarity index 100% rename from pgpainless-core/src/test/java/org/pgpainless/util/CollectionUtils.java rename to pgpainless-core/src/main/java/org/pgpainless/util/CollectionUtils.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/DateUtil.java b/pgpainless-core/src/main/java/org/pgpainless/util/DateUtil.java new file mode 100644 index 00000000..f56020d4 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/util/DateUtil.java @@ -0,0 +1,75 @@ +// 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 new file mode 100644 index 00000000..3b342778 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/util/MultiMap.java @@ -0,0 +1,137 @@ +// 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 new file mode 100644 index 00000000..11aa7651 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/util/NotationRegistry.java @@ -0,0 +1,50 @@ +// 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 new file mode 100644 index 00000000..9576fb3e --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/util/Passphrase.java @@ -0,0 +1,168 @@ +// 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 new file mode 100644 index 00000000..1e71bb03 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/util/SessionKey.java @@ -0,0 +1,66 @@ +// 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/test/java/org/pgpainless/util/Tuple.java b/pgpainless-core/src/main/java/org/pgpainless/util/Tuple.java similarity index 66% rename from pgpainless-core/src/test/java/org/pgpainless/util/Tuple.java rename to pgpainless-core/src/main/java/org/pgpainless/util/Tuple.java index 84d7a370..27ad6a12 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/util/Tuple.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/Tuple.java @@ -4,14 +4,6 @@ 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/main/java/org/pgpainless/util/selection/keyring/PublicKeyRingSelectionStrategy.java b/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/PublicKeyRingSelectionStrategy.java index 7dbf7c93..68c9d946 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,7 +7,6 @@ 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; @@ -34,9 +33,9 @@ public abstract class PublicKeyRingSelectionStrategy implements KeyRingSelect @Override public MultiMap selectKeyRingsFromCollections(@Nonnull MultiMap keyRingCollections) { MultiMap keyRings = new MultiMap<>(); - for (Map.Entry> entry : keyRingCollections.entrySet()) { - for (PGPPublicKeyRingCollection collection : entry.getValue()) { - keyRings.plus(entry.getKey(), selectKeyRingsFromCollection(entry.getKey(), collection)); + for (O identifier : keyRingCollections.keySet()) { + for (PGPPublicKeyRingCollection collection : keyRingCollections.get(identifier)) { + keyRings.put(identifier, selectKeyRingsFromCollection(identifier, 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 9e57b575..ac5e8065 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,7 +6,6 @@ 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; @@ -33,9 +32,9 @@ public abstract class SecretKeyRingSelectionStrategy implements KeyRingSelect @Override public MultiMap selectKeyRingsFromCollections(@Nonnull MultiMap keyRingCollections) { MultiMap keyRings = new MultiMap<>(); - for (Map.Entry> entry : keyRingCollections.entrySet()) { - for (PGPSecretKeyRingCollection collection : entry.getValue()) { - keyRings.plus(entry.getKey(), selectKeyRingsFromCollection(entry.getKey(), collection)); + for (O identifier : keyRingCollections.keySet()) { + for (PGPSecretKeyRingCollection collection : keyRingCollections.get(identifier)) { + keyRings.put(identifier, selectKeyRingsFromCollection(identifier, 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 new file mode 100644 index 00000000..5c1611af --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/util/selection/userid/SelectUserId.java @@ -0,0 +1,231 @@ +// 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 new file mode 100644 index 00000000..5b70f412 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/util/selection/userid/package-info.java @@ -0,0 +1,8 @@ +// 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 deleted file mode 100644 index 2763cb55..00000000 --- a/pgpainless-core/src/main/kotlin/openpgp/DateExtensions.kt +++ /dev/null @@ -1,65 +0,0 @@ -// 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 deleted file mode 100644 index c6c318b3..00000000 --- a/pgpainless-core/src/main/kotlin/openpgp/LongExtensions.kt +++ /dev/null @@ -1,21 +0,0 @@ -// 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 deleted file mode 100644 index 81d9e605..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/PGPainless.kt +++ /dev/null @@ -1,171 +0,0 @@ -// 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. - * - *
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 deleted file mode 100644 index b9474247..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/negotiation/HashAlgorithmNegotiator.kt +++ /dev/null @@ -1,73 +0,0 @@ -// 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 deleted file mode 100644 index d11f2c03..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/negotiation/SymmetricKeyAlgorithmNegotiator.kt +++ /dev/null @@ -1,81 +0,0 @@ -// 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 deleted file mode 100644 index f3d60bf6..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/authentication/CertificateAuthenticity.kt +++ /dev/null @@ -1,48 +0,0 @@ -// 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 deleted file mode 100644 index 093c2325..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/authentication/CertificateAuthority.kt +++ /dev/null @@ -1,74 +0,0 @@ -// 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 deleted file mode 100644 index 7126db66..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/PGPKeyRingExtensions.kt +++ /dev/null @@ -1,74 +0,0 @@ -// 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 deleted file mode 100644 index d267fa83..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/PGPPublicKeyExtensions.kt +++ /dev/null @@ -1,53 +0,0 @@ -// 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 deleted file mode 100644 index 84f81e64..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/PGPSecretKeyExtensions.kt +++ /dev/null @@ -1,83 +0,0 @@ -// 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 deleted file mode 100644 index 99c562e6..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/PGPSecretKeyRingExtensions.kt +++ /dev/null @@ -1,79 +0,0 @@ -// 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 deleted file mode 100644 index 1393883c..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/PGPSignatureExtensions.kt +++ /dev/null @@ -1,108 +0,0 @@ -// 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 deleted file mode 100644 index 9bafa6da..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/CachingBcPublicKeyDataDecryptorFactory.kt +++ /dev/null @@ -1,78 +0,0 @@ -// 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 deleted file mode 100644 index 39a4e8e4..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/ConsumerOptions.kt +++ /dev/null @@ -1,420 +0,0 @@ -// 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 deleted file mode 100644 index cb6254dc..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactory.kt +++ /dev/null @@ -1,27 +0,0 @@ -// 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 deleted file mode 100644 index d1d4f8b2..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/DecryptionBuilder.kt +++ /dev/null @@ -1,26 +0,0 @@ -// 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 deleted file mode 100644 index 18fd4179..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/DecryptionBuilderInterface.kt +++ /dev/null @@ -1,35 +0,0 @@ -// 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 deleted file mode 100644 index 86bd490a..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/DecryptionStream.kt +++ /dev/null @@ -1,21 +0,0 @@ -// 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 deleted file mode 100644 index 50ef3e02..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/HardwareSecurity.kt +++ /dev/null @@ -1,95 +0,0 @@ -// 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 deleted file mode 100644 index 4618882c..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/IntegrityProtectedInputStream.kt +++ /dev/null @@ -1,44 +0,0 @@ -// 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 deleted file mode 100644 index acfcba51..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MessageInspector.kt +++ /dev/null @@ -1,109 +0,0 @@ -// 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 deleted file mode 100644 index f7238391..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MessageMetadata.kt +++ /dev/null @@ -1,544 +0,0 @@ -// 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 deleted file mode 100644 index c5443ba8..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MissingKeyPassphraseStrategy.kt +++ /dev/null @@ -1,21 +0,0 @@ -// 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 deleted file mode 100644 index eb81847f..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MissingPublicKeyCallback.kt +++ /dev/null @@ -1,27 +0,0 @@ -// 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 deleted file mode 100644 index bd24b245..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt +++ /dev/null @@ -1,1105 +0,0 @@ -// 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 deleted file mode 100644 index 3e00fbb2..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/SignatureVerification.kt +++ /dev/null @@ -1,53 +0,0 @@ -// 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 deleted file mode 100644 index a9b353ca..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/TeeBCPGInputStream.kt +++ /dev/null @@ -1,138 +0,0 @@ -// 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 deleted file mode 100644 index 78614a96..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/cleartext_signatures/ClearsignedMessageUtil.kt +++ /dev/null @@ -1,159 +0,0 @@ -// 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 deleted file mode 100644 index da7c7cec..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/cleartext_signatures/InMemoryMultiPassStrategy.kt +++ /dev/null @@ -1,29 +0,0 @@ -// 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 deleted file mode 100644 index 4ef7ef3a..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/cleartext_signatures/MultiPassStrategy.kt +++ /dev/null @@ -1,72 +0,0 @@ -// 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 deleted file mode 100644 index 88fcf6f1..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/cleartext_signatures/WriteToFileMultiPassStrategy.kt +++ /dev/null @@ -1,39 +0,0 @@ -// 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 deleted file mode 100644 index 2bea3356..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/InputSymbol.kt +++ /dev/null @@ -1,30 +0,0 @@ -// 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 deleted file mode 100644 index 127d10f8..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.kt +++ /dev/null @@ -1,91 +0,0 @@ -// 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 deleted file mode 100644 index 7dff6ba2..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/PDA.kt +++ /dev/null @@ -1,127 +0,0 @@ -// 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 deleted file mode 100644 index 8f927864..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/StackSymbol.kt +++ /dev/null @@ -1,14 +0,0 @@ -// 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 deleted file mode 100644 index 8fad2fb7..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/State.kt +++ /dev/null @@ -1,14 +0,0 @@ -// 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 deleted file mode 100644 index a90e981e..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/Syntax.kt +++ /dev/null @@ -1,29 +0,0 @@ -// 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 deleted file mode 100644 index d0a09992..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/Transition.kt +++ /dev/null @@ -1,22 +0,0 @@ -// 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 deleted file mode 100644 index 47aed2be..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/BcHashContextSigner.kt +++ /dev/null @@ -1,55 +0,0 @@ -// 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 deleted file mode 100644 index bf66b6dc..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/BcPGPHashContextContentSignerBuilder.kt +++ /dev/null @@ -1,138 +0,0 @@ -// 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 deleted file mode 100644 index 76d747e0..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/CRLFGeneratorStream.kt +++ /dev/null @@ -1,50 +0,0 @@ -// 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 deleted file mode 100644 index 6b4713d6..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionBuilder.kt +++ /dev/null @@ -1,73 +0,0 @@ -// 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 deleted file mode 100644 index 2db98846..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionBuilderInterface.kt +++ /dev/null @@ -1,46 +0,0 @@ -// 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 deleted file mode 100644 index f261b85e..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionOptions.kt +++ /dev/null @@ -1,325 +0,0 @@ -// 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 deleted file mode 100644 index 4f6e6978..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionResult.kt +++ /dev/null @@ -1,103 +0,0 @@ -// 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 deleted file mode 100644 index f2617c34..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionStream.kt +++ /dev/null @@ -1,273 +0,0 @@ -// 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 deleted file mode 100644 index ef8641a7..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/PGPHashContextContentSignerBuilder.kt +++ /dev/null @@ -1,46 +0,0 @@ -// 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 deleted file mode 100644 index e77ef2e3..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/ProducerOptions.kt +++ /dev/null @@ -1,269 +0,0 @@ -// 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 deleted file mode 100644 index b2d53dcd..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/SignatureGenerationStream.kt +++ /dev/null @@ -1,32 +0,0 @@ -// 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 deleted file mode 100644 index e0fe2972..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/SigningOptions.kt +++ /dev/null @@ -1,483 +0,0 @@ -// 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 deleted file mode 100644 index dcf594ea..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/implementation/BcImplementationFactory.kt +++ /dev/null @@ -1,114 +0,0 @@ -// 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 deleted file mode 100644 index 58478379..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/implementation/ImplementationFactory.kt +++ /dev/null @@ -1,117 +0,0 @@ -// 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 deleted file mode 100644 index 865f1e0d..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/implementation/JceImplementationFactory.kt +++ /dev/null @@ -1,112 +0,0 @@ -// 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 deleted file mode 100644 index 9a2f1f7b..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/OpenPgpFingerprint.kt +++ /dev/null @@ -1,166 +0,0 @@ -// 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 deleted file mode 100644 index e02f0ae7..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/OpenPgpV4Fingerprint.kt +++ /dev/null @@ -1,72 +0,0 @@ -// 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 deleted file mode 100644 index 7bc36cc9..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/OpenPgpV5Fingerprint.kt +++ /dev/null @@ -1,27 +0,0 @@ -// 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 deleted file mode 100644 index 0b843ed2..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/OpenPgpV6Fingerprint.kt +++ /dev/null @@ -1,27 +0,0 @@ -// 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 deleted file mode 100644 index 2aec7976..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/SubkeyIdentifier.kt +++ /dev/null @@ -1,72 +0,0 @@ -// 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 deleted file mode 100644 index a34dd880..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/_64DigitFingerprint.kt +++ /dev/null @@ -1,74 +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 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 deleted file mode 100644 index 9499355c..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/certification/CertifyCertificate.kt +++ /dev/null @@ -1,245 +0,0 @@ -// 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 deleted file mode 100644 index f69d4a08..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/collection/PGPKeyRingCollection.kt +++ /dev/null @@ -1,116 +0,0 @@ -// 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 deleted file mode 100644 index aacfcceb..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingBuilder.kt +++ /dev/null @@ -1,268 +0,0 @@ -// 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 deleted file mode 100644 index ecc818b6..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingBuilderInterface.kt +++ /dev/null @@ -1,37 +0,0 @@ -// 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 deleted file mode 100644 index 82743661..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingTemplates.kt +++ /dev/null @@ -1,213 +0,0 @@ -// 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 deleted file mode 100644 index f616a7f2..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeySpec.kt +++ /dev/null @@ -1,28 +0,0 @@ -// 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 deleted file mode 100644 index 0e7f9aae..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeySpecBuilder.kt +++ /dev/null @@ -1,69 +0,0 @@ -// 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 deleted file mode 100644 index 7fb767e4..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeySpecBuilderInterface.kt +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.generation - -import java.util.* -import org.pgpainless.algorithm.CompressionAlgorithm -import org.pgpainless.algorithm.HashAlgorithm -import org.pgpainless.algorithm.SymmetricKeyAlgorithm - -interface KeySpecBuilderInterface { - - fun overridePreferredCompressionAlgorithms( - vararg algorithms: CompressionAlgorithm - ): KeySpecBuilder - - fun overridePreferredHashAlgorithms(vararg algorithms: HashAlgorithm): KeySpecBuilder - - fun overridePreferredSymmetricKeyAlgorithms( - vararg algorithms: SymmetricKeyAlgorithm - ): KeySpecBuilder - - fun setKeyCreationDate(creationDate: Date): KeySpecBuilder - - fun build(): KeySpec -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/KeyType.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/KeyType.kt deleted file mode 100644 index c7691f46..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/KeyType.kt +++ /dev/null @@ -1,107 +0,0 @@ -// 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 deleted file mode 100644 index 9e229462..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/ecc/EllipticCurve.kt +++ /dev/null @@ -1,27 +0,0 @@ -// 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 deleted file mode 100644 index 04e196e0..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/ecc/ecdh/ECDH.kt +++ /dev/null @@ -1,21 +0,0 @@ -// 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 deleted file mode 100644 index 1784b49d..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/ecc/ecdsa/ECDSA.kt +++ /dev/null @@ -1,21 +0,0 @@ -// 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 deleted file mode 100644 index e177de68..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/eddsa_legacy/EdDSALegacy.kt +++ /dev/null @@ -1,20 +0,0 @@ -// 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 deleted file mode 100644 index 2a32b8aa..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/eddsa_legacy/EdDSALegacyCurve.kt +++ /dev/null @@ -1,12 +0,0 @@ -// 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 deleted file mode 100644 index d925fc3d..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/elgamal/ElGamal.kt +++ /dev/null @@ -1,27 +0,0 @@ -// 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 deleted file mode 100644 index 2d29b88d..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/elgamal/ElGamalLength.kt +++ /dev/null @@ -1,63 +0,0 @@ -// 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 deleted file mode 100644 index 39ddbbbb..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/rsa/RSA.kt +++ /dev/null @@ -1,22 +0,0 @@ -// 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 deleted file mode 100644 index 7837a1f5..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/rsa/RsaLength.kt +++ /dev/null @@ -1,17 +0,0 @@ -// 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 deleted file mode 100644 index 4f0408bc..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/xdh_legacy/XDHLegacy.kt +++ /dev/null @@ -1,20 +0,0 @@ -// 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 deleted file mode 100644 index cb634850..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/xdh_legacy/XDHLegacySpec.kt +++ /dev/null @@ -1,12 +0,0 @@ -// 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 deleted file mode 100644 index 935c4f48..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyAccessor.kt +++ /dev/null @@ -1,89 +0,0 @@ -// 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 deleted file mode 100644 index 75a35140..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyInfo.kt +++ /dev/null @@ -1,81 +0,0 @@ -// 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 deleted file mode 100644 index ce4fbe56..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyRingInfo.kt +++ /dev/null @@ -1,766 +0,0 @@ -// 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 deleted file mode 100644 index 5480442d..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.kt +++ /dev/null @@ -1,785 +0,0 @@ -// 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 deleted file mode 100644 index 140ff905..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.kt +++ /dev/null @@ -1,678 +0,0 @@ -// 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 deleted file mode 100644 index 6f6bde61..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/parsing/KeyRingReader.kt +++ /dev/null @@ -1,337 +0,0 @@ -// 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 deleted file mode 100644 index c5db2086..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/BaseSecretKeyRingProtector.kt +++ /dev/null @@ -1,44 +0,0 @@ -// 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 deleted file mode 100644 index 20704685..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/CachingSecretKeyRingProtector.kt +++ /dev/null @@ -1,167 +0,0 @@ -// 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 deleted file mode 100644 index c7566f6d..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/KeyRingProtectionSettings.kt +++ /dev/null @@ -1,55 +0,0 @@ -// 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 deleted file mode 100644 index 9eb47e88..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/PasswordBasedSecretKeyRingProtector.kt +++ /dev/null @@ -1,73 +0,0 @@ -// 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 deleted file mode 100644 index 5e86d950..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/SecretKeyRingProtector.kt +++ /dev/null @@ -1,156 +0,0 @@ -// 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 deleted file mode 100644 index b3b0308f..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/UnlockSecretKey.kt +++ /dev/null @@ -1,78 +0,0 @@ -// 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 deleted file mode 100644 index a25bb31a..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/UnprotectedKeysProtector.kt +++ /dev/null @@ -1,16 +0,0 @@ -// 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 deleted file mode 100644 index a1a9f6c2..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/fixes/S2KUsageFix.kt +++ /dev/null @@ -1,85 +0,0 @@ -// 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 deleted file mode 100644 index 3457cff7..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/passphrase_provider/MapBasedPassphraseProvider.kt +++ /dev/null @@ -1,22 +0,0 @@ -// 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 deleted file mode 100644 index a80b8bb0..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/passphrase_provider/SecretKeyPassphraseProvider.kt +++ /dev/null @@ -1,36 +0,0 @@ -// 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 deleted file mode 100644 index a9f6801d..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/passphrase_provider/SolitaryPassphraseProvider.kt +++ /dev/null @@ -1,15 +0,0 @@ -// 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 deleted file mode 100644 index 3f1b98b1..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyIdUtil.kt +++ /dev/null @@ -1,35 +0,0 @@ -// 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 deleted file mode 100644 index f83b5486..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyRingUtils.kt +++ /dev/null @@ -1,528 +0,0 @@ -// 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 deleted file mode 100644 index a1e79bf3..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/PublicKeyParameterValidationUtil.kt +++ /dev/null @@ -1,286 +0,0 @@ -// 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 deleted file mode 100644 index c2b81700..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/UserId.kt +++ /dev/null @@ -1,217 +0,0 @@ -// 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 deleted file mode 100644 index 7c6bb2d3..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/policy/Policy.kt +++ /dev/null @@ -1,410 +0,0 @@ -// 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 deleted file mode 100644 index 27192953..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/provider/BouncyCastleProviderFactory.kt +++ /dev/null @@ -1,12 +0,0 @@ -// 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 deleted file mode 100644 index 531ae54b..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/provider/ProviderFactory.kt +++ /dev/null @@ -1,33 +0,0 @@ -// 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 deleted file mode 100644 index 770dfc56..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/SignatureUtils.kt +++ /dev/null @@ -1,274 +0,0 @@ -// 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 deleted file mode 100644 index eaf05df1..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/AbstractSignatureBuilder.kt +++ /dev/null @@ -1,135 +0,0 @@ -// 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 deleted file mode 100644 index c4d11ea9..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/DirectKeySelfSignatureBuilder.kt +++ /dev/null @@ -1,50 +0,0 @@ -// 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 deleted file mode 100644 index f34d37a7..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/PrimaryKeyBindingSignatureBuilder.kt +++ /dev/null @@ -1,61 +0,0 @@ -// 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 deleted file mode 100644 index 36589613..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/RevocationSignatureBuilder.kt +++ /dev/null @@ -1,70 +0,0 @@ -// 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 deleted file mode 100644 index 370479c8..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/SelfSignatureBuilder.kt +++ /dev/null @@ -1,69 +0,0 @@ -// 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 deleted file mode 100644 index 6e2694e3..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/SubkeyBindingSignatureBuilder.kt +++ /dev/null @@ -1,73 +0,0 @@ -// 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 deleted file mode 100644 index be45dc8a..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/ThirdPartyCertificationSignatureBuilder.kt +++ /dev/null @@ -1,117 +0,0 @@ -// 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 deleted file mode 100644 index 32d677d5..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/ThirdPartyDirectKeySignatureBuilder.kt +++ /dev/null @@ -1,57 +0,0 @@ -// 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 deleted file mode 100644 index a1aa57b2..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/UniversalSignatureBuilder.kt +++ /dev/null @@ -1,52 +0,0 @@ -// 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 deleted file mode 100644 index 83b7e54e..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/CertificateValidator.kt +++ /dev/null @@ -1,315 +0,0 @@ -// 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 deleted file mode 100644 index 4a89e0b2..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/OnePassSignatureCheck.kt +++ /dev/null @@ -1,34 +0,0 @@ -// 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/kotlin/org/pgpainless/signature/consumer/SignatureCheck.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureCheck.kt deleted file mode 100644 index 15564773..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureCheck.kt +++ /dev/null @@ -1,25 +0,0 @@ -// 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 deleted file mode 100644 index a913bf32..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureCreationDateComparator.kt +++ /dev/null @@ -1,30 +0,0 @@ -// 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 deleted file mode 100644 index 5952003e..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignaturePicker.kt +++ /dev/null @@ -1,383 +0,0 @@ -// 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 deleted file mode 100644 index 7cc384e1..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureValidator.kt +++ /dev/null @@ -1,701 +0,0 @@ -// 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 deleted file mode 100644 index 1153b875..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureValidityComparator.kt +++ /dev/null @@ -1,32 +0,0 @@ -// 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 deleted file mode 100644 index d51b2379..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureVerifier.kt +++ /dev/null @@ -1,597 +0,0 @@ -// 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 deleted file mode 100644 index b9d7fb3f..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/BaseSignatureSubpackets.kt +++ /dev/null @@ -1,207 +0,0 @@ -// 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 deleted file mode 100644 index bb1d6550..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/CertificationSubpackets.kt +++ /dev/null @@ -1,58 +0,0 @@ -// 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 deleted file mode 100644 index 79807322..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/RevocationSignatureSubpackets.kt +++ /dev/null @@ -1,82 +0,0 @@ -// 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 deleted file mode 100644 index 318b7adf..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SelfSignatureSubpackets.kt +++ /dev/null @@ -1,182 +0,0 @@ -// 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 deleted file mode 100644 index fbc56035..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpacketCallback.kt +++ /dev/null @@ -1,26 +0,0 @@ -// 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 deleted file mode 100644 index 886cedb6..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpackets.kt +++ /dev/null @@ -1,639 +0,0 @@ -// 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 deleted file mode 100644 index 8a6c16bf..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpacketsHelper.kt +++ /dev/null @@ -1,210 +0,0 @@ -// 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 deleted file mode 100644 index dcc85630..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.kt +++ /dev/null @@ -1,596 +0,0 @@ -// 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 deleted file mode 100644 index db1cb54d..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmorUtils.kt +++ /dev/null @@ -1,434 +0,0 @@ -// 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 deleted file mode 100644 index b1956cf6..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmoredInputStreamFactory.kt +++ /dev/null @@ -1,43 +0,0 @@ -// 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 deleted file mode 100644 index caf14e53..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmoredOutputStreamFactory.kt +++ /dev/null @@ -1,105 +0,0 @@ -// 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 deleted file mode 100644 index 712ac262..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/util/DateUtil.kt +++ /dev/null @@ -1,47 +0,0 @@ -// 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 deleted file mode 100644 index 3aa22d0d..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/util/MultiMap.kt +++ /dev/null @@ -1,88 +0,0 @@ -// 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 deleted file mode 100644 index 96200b30..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/util/NotationRegistry.kt +++ /dev/null @@ -1,43 +0,0 @@ -// 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 deleted file mode 100644 index 5a3df702..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/util/NullOutputStream.kt +++ /dev/null @@ -1,31 +0,0 @@ -// 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 deleted file mode 100644 index bd25f2b9..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/util/Passphrase.kt +++ /dev/null @@ -1,102 +0,0 @@ -// 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 deleted file mode 100644 index ef8eb9e7..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/util/SessionKey.kt +++ /dev/null @@ -1,49 +0,0 @@ -// 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 deleted file mode 100644 index f2794925..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/util/selection/userid/SelectUserId.kt +++ /dev/null @@ -1,101 +0,0 @@ -// 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/OnePassSignatureVerificationWithPartialLengthLiteralDataRegressionTest.java b/pgpainless-core/src/test/java/investigations/OnePassSignatureVerificationWithPartialLengthLiteralDataRegressionTest.java index 7ec53edb..a9b20e9e 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.getMetadata(); + decryptionStream.getResult(); } } 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 7f2bc2a6..1bf78776 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/algorithm/SignatureTypeTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/algorithm/SignatureTypeTest.java @@ -5,6 +5,7 @@ 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; @@ -30,6 +31,6 @@ public class SignatureTypeTest { assertFalse(SignatureType.isRevocationSignature(SignatureType.STANDALONE.getCode())); assertFalse(SignatureType.isRevocationSignature(SignatureType.TIMESTAMP.getCode())); - assertFalse(SignatureType.isRevocationSignature(-3)); + assertThrows(IllegalArgumentException.class, () -> 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 f9936db0..c427af99 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,7 +13,6 @@ 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; @@ -31,9 +30,6 @@ 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; @@ -126,9 +122,9 @@ public class CanonicalizedDataEncryptionTest { @Test public void noInputEncodingBinaryDataBinarySig() throws PGPException, IOException { String msg = encryptAndSign(message, DocumentSignatureType.BINARY_DOCUMENT, StreamEncoding.BINARY, false); - MessageMetadata metadata = decryptAndVerify(msg); + OpenPgpMetadata metadata = decryptAndVerify(msg); - if (!metadata.isVerifiedSigned()) { + if (!metadata.isVerified()) { // CHECKSTYLE:OFF System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); System.out.println(msg); @@ -140,9 +136,9 @@ public class CanonicalizedDataEncryptionTest { @Test public void noInputEncodingBinaryDataTextSig() throws PGPException, IOException { String msg = encryptAndSign(message, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT, StreamEncoding.BINARY, false); - MessageMetadata metadata = decryptAndVerify(msg); + OpenPgpMetadata metadata = decryptAndVerify(msg); - if (!metadata.isVerifiedSigned()) { + if (!metadata.isVerified()) { // CHECKSTYLE:OFF System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); System.out.println(msg); @@ -154,9 +150,9 @@ public class CanonicalizedDataEncryptionTest { @Test public void noInputEncodingTextDataBinarySig() throws PGPException, IOException { String msg = encryptAndSign(message, DocumentSignatureType.BINARY_DOCUMENT, StreamEncoding.TEXT, false); - MessageMetadata metadata = decryptAndVerify(msg); + OpenPgpMetadata metadata = decryptAndVerify(msg); - if (!metadata.isVerifiedSigned()) { + if (!metadata.isVerified()) { // CHECKSTYLE:OFF System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); System.out.println(msg); @@ -168,9 +164,9 @@ public class CanonicalizedDataEncryptionTest { @Test public void noInputEncodingTextDataTextSig() throws PGPException, IOException { String msg = encryptAndSign(message, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT, StreamEncoding.TEXT, false); - MessageMetadata metadata = decryptAndVerify(msg); + OpenPgpMetadata metadata = decryptAndVerify(msg); - if (!metadata.isVerifiedSigned()) { + if (!metadata.isVerified()) { // CHECKSTYLE:OFF System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); System.out.println(msg); @@ -182,9 +178,9 @@ public class CanonicalizedDataEncryptionTest { @Test public void noInputEncodingUtf8DataBinarySig() throws PGPException, IOException { String msg = encryptAndSign(message, DocumentSignatureType.BINARY_DOCUMENT, StreamEncoding.UTF8, false); - MessageMetadata metadata = decryptAndVerify(msg); + OpenPgpMetadata metadata = decryptAndVerify(msg); - if (!metadata.isVerifiedSigned()) { + if (!metadata.isVerified()) { // CHECKSTYLE:OFF System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); System.out.println(msg); @@ -196,9 +192,9 @@ public class CanonicalizedDataEncryptionTest { @Test public void noInputEncodingUtf8DataTextSig() throws PGPException, IOException { String msg = encryptAndSign(message, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT, StreamEncoding.UTF8, false); - MessageMetadata metadata = decryptAndVerify(msg); + OpenPgpMetadata metadata = decryptAndVerify(msg); - if (!metadata.isVerifiedSigned()) { + if (!metadata.isVerified()) { // CHECKSTYLE:OFF System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); System.out.println(msg); @@ -211,9 +207,9 @@ public class CanonicalizedDataEncryptionTest { @Test public void inputEncodingBinaryDataBinarySig() throws PGPException, IOException { String msg = encryptAndSign(message, DocumentSignatureType.BINARY_DOCUMENT, StreamEncoding.BINARY, true); - MessageMetadata metadata = decryptAndVerify(msg); + OpenPgpMetadata metadata = decryptAndVerify(msg); - if (!metadata.isVerifiedSigned()) { + if (!metadata.isVerified()) { // CHECKSTYLE:OFF System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); System.out.println(msg); @@ -225,9 +221,9 @@ public class CanonicalizedDataEncryptionTest { @Test public void inputEncodingBinaryDataTextSig() throws PGPException, IOException { String msg = encryptAndSign(message, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT, StreamEncoding.BINARY, true); - MessageMetadata metadata = decryptAndVerify(msg); + OpenPgpMetadata metadata = decryptAndVerify(msg); - if (!metadata.isVerifiedSigned()) { + if (!metadata.isVerified()) { // CHECKSTYLE:OFF System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); System.out.println(msg); @@ -239,9 +235,9 @@ public class CanonicalizedDataEncryptionTest { @Test public void inputEncodingTextDataBinarySig() throws PGPException, IOException { String msg = encryptAndSign(message, DocumentSignatureType.BINARY_DOCUMENT, StreamEncoding.TEXT, true); - MessageMetadata metadata = decryptAndVerify(msg); + OpenPgpMetadata metadata = decryptAndVerify(msg); - if (!metadata.isVerifiedSigned()) { + if (!metadata.isVerified()) { // CHECKSTYLE:OFF System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); System.out.println(msg); @@ -253,9 +249,9 @@ public class CanonicalizedDataEncryptionTest { @Test public void inputEncodingTextDataTextSig() throws PGPException, IOException { String msg = encryptAndSign(message, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT, StreamEncoding.TEXT, true); - MessageMetadata metadata = decryptAndVerify(msg); + OpenPgpMetadata metadata = decryptAndVerify(msg); - if (!metadata.isVerifiedSigned()) { + if (!metadata.isVerified()) { // CHECKSTYLE:OFF System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); System.out.println(msg); @@ -267,9 +263,9 @@ public class CanonicalizedDataEncryptionTest { @Test public void inputEncodingUtf8DataBinarySig() throws PGPException, IOException { String msg = encryptAndSign(message, DocumentSignatureType.BINARY_DOCUMENT, StreamEncoding.UTF8, true); - MessageMetadata metadata = decryptAndVerify(msg); + OpenPgpMetadata metadata = decryptAndVerify(msg); - if (!metadata.isVerifiedSigned()) { + if (!metadata.isVerified()) { // CHECKSTYLE:OFF System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); System.out.println(msg); @@ -281,9 +277,9 @@ public class CanonicalizedDataEncryptionTest { @Test public void inputEncodingUtf8DataTextSig() throws PGPException, IOException { String msg = encryptAndSign(message, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT, StreamEncoding.UTF8, true); - MessageMetadata metadata = decryptAndVerify(msg); + OpenPgpMetadata metadata = decryptAndVerify(msg); - if (!metadata.isVerifiedSigned()) { + if (!metadata.isVerified()) { // CHECKSTYLE:OFF System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); System.out.println(msg); @@ -292,9 +288,11 @@ public class CanonicalizedDataEncryptionTest { } } - @ParameterizedTest - @MethodSource("resultOfDecryptionIsCRLFEncodedArguments") - public void resultOfDecryptionIsCRLFEncoded(String before, String after) throws PGPException, IOException { + @Test + public void resultOfDecryptionIsCRLFEncoded() throws PGPException, IOException { + String before = "Foo\nBar!\n"; + String after = "Foo\r\nBar!\r\n"; + String encrypted = encryptAndSign(before, DocumentSignatureType.BINARY_DOCUMENT, StreamEncoding.TEXT, true); ByteArrayInputStream in = new ByteArrayInputStream(encrypted.getBytes(StandardCharsets.UTF_8)); @@ -311,16 +309,6 @@ 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"; @@ -372,7 +360,7 @@ public class CanonicalizedDataEncryptionTest { return msg; } - private MessageMetadata decryptAndVerify(String msg) throws PGPException, IOException { + private OpenPgpMetadata decryptAndVerify(String msg) throws PGPException, IOException { ByteArrayInputStream in = new ByteArrayInputStream(msg.getBytes(StandardCharsets.UTF_8)); DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() .onInputStream(in) @@ -383,7 +371,7 @@ public class CanonicalizedDataEncryptionTest { Streams.drain(decryptionStream); decryptionStream.close(); - return decryptionStream.getMetadata(); + return decryptionStream.getResult(); } @Test @@ -451,8 +439,8 @@ public class CanonicalizedDataEncryptionTest { Streams.pipeAll(decryptionStream, decrypted); decryptionStream.close(); - MessageMetadata metadata = decryptionStream.getMetadata(); - assertTrue(metadata.isVerifiedSigned(), "Not verified! Sig Type: " + sigType + " StreamEncoding: " + streamEncoding); + OpenPgpMetadata metadata = decryptionStream.getResult(); + assertTrue(metadata.isVerified(), "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 720a0d53..cabfdbb1 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(); - MessageMetadata result = decryptionStream.getMetadata(); - assertTrue(result.isVerifiedSigned()); - assertTrue(result.isUsingCleartextSignatureFramework()); + OpenPgpMetadata result = decryptionStream.getResult(); + assertTrue(result.isVerified()); + assertTrue(result.isCleartextSigned()); - PGPSignature signature = result.getVerifiedSignatures().iterator().next().getSignature(); + PGPSignature signature = result.getVerifiedSignatures().values().iterator().next(); assertEquals(signature.getKeyID(), signingKeys.getPublicKey().getKeyID()); assertArrayEquals(MESSAGE_BODY, out.toByteArray()); @@ -125,10 +125,10 @@ public class CleartextSignatureVerificationTest { Streams.pipeAll(decryptionStream, out); decryptionStream.close(); - MessageMetadata result = decryptionStream.getMetadata(); - assertTrue(result.isVerifiedSigned()); + OpenPgpMetadata result = decryptionStream.getResult(); + assertTrue(result.isVerified()); - PGPSignature signature = result.getVerifiedSignatures().iterator().next().getSignature(); + PGPSignature signature = result.getVerifiedSignatures().values().iterator().next(); assertEquals(signature.getKeyID(), signingKeys.getPublicKey().getKeyID()); FileInputStream fileIn = new FileInputStream(file); @@ -178,7 +178,7 @@ public class CleartextSignatureVerificationTest { Streams.pipeAll(decryptionStream, out); decryptionStream.close(); - MessageMetadata metadata = decryptionStream.getMetadata(); + OpenPgpMetadata metadata = decryptionStream.getResult(); assertEquals(1, metadata.getVerifiedSignatures().size()); } @@ -210,8 +210,8 @@ public class CleartextSignatureVerificationTest { Streams.pipeAll(verificationStream, msgOut); verificationStream.close(); - MessageMetadata metadata = verificationStream.getMetadata(); - assertTrue(metadata.isVerifiedSigned()); + OpenPgpMetadata metadata = verificationStream.getResult(); + assertTrue(metadata.isVerified()); } @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 71fbf9be..73c3bf56 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, int pkeskVersion) + public byte[] decryptSessionKey(long keyId, int keyAlgorithm, byte[] sessionKeyData) throws HardwareSecurity.HardwareSecurityException { // Emulate hardware decryption. try { PGPSecretKey decryptionKey = secretKey.getSecretKey(encryptionKey.getKeyID()); PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(decryptionKey, Passphrase.emptyPassphrase()); PublicKeyDataDecryptorFactory internal = new BcPublicKeyDataDecryptorFactory(privateKey); - return internal.recoverSessionData(keyAlgorithm, new byte[][] {sessionKeyData}, pkeskVersion); + return internal.recoverSessionData(keyAlgorithm, new byte[][] {sessionKeyData}); } 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 82796cb9..dec10c0a 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(); - MessageMetadata metadata = decryptor.getMetadata(); + OpenPgpMetadata metadata = decryptor.getResult(); byte[] expected = TestKeys.TEST_MESSAGE_01_PLAIN.getBytes(UTF8); byte[] actual = toPlain.toByteArray(); @@ -72,13 +72,14 @@ public class DecryptAndVerifyMessageTest { assertArrayEquals(expected, actual); assertTrue(metadata.isEncrypted()); - assertFalse(metadata.isUsingCleartextSignatureFramework()); - assertTrue(metadata.isVerifiedSigned()); + assertTrue(metadata.isSigned()); + assertFalse(metadata.isCleartextSigned()); + assertTrue(metadata.isVerified()); assertEquals(CompressionAlgorithm.ZLIB, metadata.getCompressionAlgorithm()); - assertEquals(SymmetricKeyAlgorithm.AES_256, metadata.getEncryptionAlgorithm()); + assertEquals(SymmetricKeyAlgorithm.AES_256, metadata.getSymmetricKeyAlgorithm()); + assertEquals(1, metadata.getSignatures().size()); assertEquals(1, metadata.getVerifiedSignatures().size()); - assertEquals(1, metadata.getVerifiedSignatures().size()); - assertTrue(metadata.isVerifiedSignedBy(TestKeys.JULIET_FINGERPRINT)); + assertTrue(metadata.containsVerifiedSignatureFrom(TestKeys.JULIET_FINGERPRINT)); assertEquals(new SubkeyIdentifier(TestKeys.JULIET_FINGERPRINT), metadata.getDecryptionKey()); } @@ -121,7 +122,7 @@ public class DecryptAndVerifyMessageTest { decryptor.close(); toPlain.close(); - MessageMetadata metadata = decryptor.getMetadata(); + OpenPgpMetadata metadata = decryptor.getResult(); byte[] expected = TestKeys.TEST_MESSAGE_01_PLAIN.getBytes(UTF8); byte[] actual = toPlain.toByteArray(); @@ -129,13 +130,14 @@ public class DecryptAndVerifyMessageTest { assertArrayEquals(expected, actual); assertTrue(metadata.isEncrypted()); - assertFalse(metadata.isUsingCleartextSignatureFramework()); - assertTrue(metadata.isVerifiedSigned()); + assertTrue(metadata.isSigned()); + assertFalse(metadata.isCleartextSigned()); + assertTrue(metadata.isVerified()); assertEquals(CompressionAlgorithm.ZLIB, metadata.getCompressionAlgorithm()); - assertEquals(SymmetricKeyAlgorithm.AES_256, metadata.getEncryptionAlgorithm()); + assertEquals(SymmetricKeyAlgorithm.AES_256, metadata.getSymmetricKeyAlgorithm()); + assertEquals(1, metadata.getSignatures().size()); assertEquals(1, metadata.getVerifiedSignatures().size()); - assertEquals(1, metadata.getVerifiedSignatures().size()); - assertTrue(metadata.isVerifiedSignedBy(TestKeys.JULIET_FINGERPRINT)); + assertTrue(metadata.containsVerifiedSignatureFrom(TestKeys.JULIET_FINGERPRINT)); assertEquals(new SubkeyIdentifier(TestKeys.JULIET_FINGERPRINT), metadata.getDecryptionKey()); } @@ -153,6 +155,6 @@ public class DecryptAndVerifyMessageTest { () -> PGPainless.decryptAndOrVerify() .onInputStream(ciphertextIn) .withOptions(ConsumerOptions.get() - .addMessagePassphrase(Passphrase.fromPassword("sw0rdf1sh")))); + .addDecryptionPassphrase(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 4eb7b203..5400d17c 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,9 +139,8 @@ public class DecryptHiddenRecipientMessageTest { Streams.pipeAll(decryptionStream, out); decryptionStream.close(); - MessageMetadata metadata = decryptionStream.getMetadata(); - assertEquals(1, metadata.getRecipientKeyIds().size()); - assertEquals(0L, metadata.getRecipientKeyIds().get(0)); + OpenPgpMetadata metadata = decryptionStream.getResult(); + assertEquals(0, metadata.getRecipientKeyIds().size()); 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 2b222c83..aa1da741 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"; - MessageMetadata metadata = verifySignature(cert, BASE_CASE); + OpenPgpMetadata metadata = verifySignature(cert, BASE_CASE); - assertTrue(metadata.isVerifiedSigned()); + assertTrue(metadata.isVerified()); } @Test @@ -137,9 +137,9 @@ public class IgnoreUnknownSignatureVersionsTest { "ou1uiXJaDzZ6wQfB\n" + "=/JL1\n" + "-----END PGP SIGNATURE-----\n"; - MessageMetadata metadata = verifySignature(cert, SIG4SIG23); + OpenPgpMetadata metadata = verifySignature(cert, SIG4SIG23); - assertTrue(metadata.isVerifiedSigned()); + assertTrue(metadata.isVerified()); } @Test @@ -170,12 +170,12 @@ public class IgnoreUnknownSignatureVersionsTest { "ou1uiXJaDzZ6wQfB\n" + "=Yc8d\n" + "-----END PGP SIGNATURE-----\n"; - MessageMetadata metadata = verifySignature(cert, SIG23SIG4); + OpenPgpMetadata metadata = verifySignature(cert, SIG23SIG4); - assertTrue(metadata.isVerifiedSigned()); + assertTrue(metadata.isVerified()); } - private MessageMetadata verifySignature(PGPPublicKeyRing cert, String BASE_CASE) throws PGPException, IOException { + private OpenPgpMetadata 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.getMetadata(); + return decryptionStream.getResult(); } } 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 7c443829..771cb8f1 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 compressed. + // and randomness cannot be encrypted. // For the sake of testing though, this is okay. MessageMetadata.Message message = new MessageMetadata.Message(); - MessageMetadata.CompressedData compressedData = new MessageMetadata.CompressedData(CompressionAlgorithm.ZIP, message.getDepth() + 1); - MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData(SymmetricKeyAlgorithm.AES_128, compressedData.getDepth() + 1); - MessageMetadata.EncryptedData encryptedData1 = new MessageMetadata.EncryptedData(SymmetricKeyAlgorithm.AES_256, encryptedData.getDepth() + 1); + MessageMetadata.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.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 42562713..f7d36aba 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,6 +7,7 @@ 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; @@ -58,6 +59,29 @@ 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 6102372a..a0ec7c25 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() - .addMessagePassphrase(Passphrase.fromPassword(PASSPHRASE))) + .addPassphrase(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() - .addMessagePassphrase(Passphrase.fromPassword(PASSPHRASE))); + .addDecryptionPassphrase(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() - .addMessagePassphrase(Passphrase.fromPassword(PASSPHRASE))); + .addDecryptionPassphrase(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 8489da9a..c86a823c 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() - .addMessagePassphrase(PASSPHRASE) + .addDecryptionPassphrase(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 f06f0233..04a98265 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(); - MessageMetadata metadata = decryptionStream.getMetadata(); + OpenPgpMetadata metadata = decryptionStream.getResult(); assertEquals(new SubkeyIdentifier(secretKeys, secretKeys.getPublicKey().getKeyID()), metadata.getDecryptionKey()); } @@ -200,7 +200,7 @@ public class PreventDecryptionUsingNonEncryptionKeyTest { Streams.drain(decryptionStream); decryptionStream.close(); - MessageMetadata metadata = decryptionStream.getMetadata(); + OpenPgpMetadata metadata = decryptionStream.getResult(); 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 9f85b241..27bc9954 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(); - MessageMetadata metadata = verificationStream.getMetadata(); + OpenPgpMetadata metadata = verificationStream.getResult(); - 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."); + 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."); } } 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 e1406f87..fa1427d3 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(); - MessageMetadata metadata = verifier.getMetadata(); - assertTrue(metadata.isVerifiedSigned()); + OpenPgpMetadata metadata = verifier.getResult(); + assertTrue(metadata.isVerified()); } @Test @@ -140,7 +140,7 @@ public class VerifyDetachedSignatureTest { Streams.drain(verifier); verifier.close(); - MessageMetadata metadata = verifier.getMetadata(); - assertTrue(metadata.isVerifiedSigned()); + OpenPgpMetadata metadata = verifier.getResult(); + assertTrue(metadata.isVerified()); } } 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 069a5f2d..d67b3d95 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); - MessageMetadata metadata = processSignedData(verifier); - assertTrue(metadata.isVerifiedSignedBy(certificate)); + OpenPgpMetadata metadata = processSignedData(verifier); + assertTrue(metadata.getVerifiedSignatures().containsKey(new SubkeyIdentifier(certificate))); } @Test @@ -81,8 +81,8 @@ public class VerifyNotBeforeNotAfterTest { .onInputStream(new ByteArrayInputStream(data)) .withOptions(options); - MessageMetadata metadata = processSignedData(verifier); - assertTrue(metadata.isVerifiedSignedBy(certificate)); + OpenPgpMetadata metadata = processSignedData(verifier); + assertTrue(metadata.containsVerifiedSignatureFrom(certificate)); } @Test @@ -93,8 +93,8 @@ public class VerifyNotBeforeNotAfterTest { DecryptionStream verifier = PGPainless.decryptAndOrVerify() .onInputStream(new ByteArrayInputStream(inlineSigned)) .withOptions(options); - MessageMetadata metadata = processSignedData(verifier); - assertTrue(metadata.isVerifiedSignedBy(certificate)); + OpenPgpMetadata metadata = processSignedData(verifier); + assertTrue(metadata.getVerifiedSignatures().containsKey(signingKey)); } @Test @@ -106,8 +106,8 @@ public class VerifyNotBeforeNotAfterTest { DecryptionStream verifier = PGPainless.decryptAndOrVerify() .onInputStream(new ByteArrayInputStream(data)) .withOptions(options); - MessageMetadata metadata = processSignedData(verifier); - assertTrue(metadata.isVerifiedSignedBy(certificate)); + OpenPgpMetadata metadata = processSignedData(verifier); + assertTrue(metadata.getVerifiedSignatures().containsKey(signingKey)); } @Test @@ -118,8 +118,8 @@ public class VerifyNotBeforeNotAfterTest { DecryptionStream verifier = PGPainless.decryptAndOrVerify() .onInputStream(new ByteArrayInputStream(inlineSigned)) .withOptions(options); - MessageMetadata metadata = processSignedData(verifier); - assertFalse(metadata.isVerifiedInlineSignedBy(certificate)); + OpenPgpMetadata metadata = processSignedData(verifier); + assertFalse(metadata.getVerifiedSignatures().containsKey(signingKey)); } @Test @@ -131,8 +131,8 @@ public class VerifyNotBeforeNotAfterTest { DecryptionStream verifier = PGPainless.decryptAndOrVerify() .onInputStream(new ByteArrayInputStream(data)) .withOptions(options); - MessageMetadata metadata = processSignedData(verifier); - assertFalse(metadata.isVerifiedSignedBy(certificate)); + OpenPgpMetadata metadata = processSignedData(verifier); + assertFalse(metadata.getVerifiedSignatures().containsKey(signingKey)); } @Test @@ -143,8 +143,8 @@ public class VerifyNotBeforeNotAfterTest { DecryptionStream verifier = PGPainless.decryptAndOrVerify() .onInputStream(new ByteArrayInputStream(inlineSigned)) .withOptions(options); - MessageMetadata metadata = processSignedData(verifier); - assertTrue(metadata.isVerifiedSignedBy(certificate)); + OpenPgpMetadata metadata = processSignedData(verifier); + assertTrue(metadata.getVerifiedSignatures().containsKey(signingKey)); } @Test @@ -156,8 +156,8 @@ public class VerifyNotBeforeNotAfterTest { DecryptionStream verifier = PGPainless.decryptAndOrVerify() .onInputStream(new ByteArrayInputStream(data)) .withOptions(options); - MessageMetadata metadata = processSignedData(verifier); - assertTrue(metadata.isVerifiedSignedBy(certificate)); + OpenPgpMetadata metadata = processSignedData(verifier); + assertTrue(metadata.getVerifiedSignatures().containsKey(signingKey)); } @Test @@ -168,8 +168,8 @@ public class VerifyNotBeforeNotAfterTest { DecryptionStream verifier = PGPainless.decryptAndOrVerify() .onInputStream(new ByteArrayInputStream(inlineSigned)) .withOptions(options); - MessageMetadata metadata = processSignedData(verifier); - assertFalse(metadata.isVerifiedSignedBy(certificate)); + OpenPgpMetadata metadata = processSignedData(verifier); + assertFalse(metadata.getVerifiedSignatures().containsKey(signingKey)); } @Test @@ -181,13 +181,13 @@ public class VerifyNotBeforeNotAfterTest { DecryptionStream verifier = PGPainless.decryptAndOrVerify() .onInputStream(new ByteArrayInputStream(data)) .withOptions(options); - MessageMetadata metadata = processSignedData(verifier); - assertFalse(metadata.isVerifiedSignedBy(certificate)); + OpenPgpMetadata metadata = processSignedData(verifier); + assertFalse(metadata.getVerifiedSignatures().containsKey(signingKey)); } - private MessageMetadata processSignedData(DecryptionStream verifier) throws IOException { + private OpenPgpMetadata processSignedData(DecryptionStream verifier) throws IOException { Streams.drain(verifier); verifier.close(); - return verifier.getMetadata(); + return verifier.getResult(); } } 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 deleted file mode 100644 index 3d2ea092..00000000 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifySignatureByCertificationKeyFailsTest.java +++ /dev/null @@ -1,228 +0,0 @@ -// 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 6b9d9cab..2a12e74a 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); - MessageMetadata metadata = processSignedData(verifier); - assertTrue(metadata.isVerifiedSignedBy(TestKeys.getEmilPublicKeyRing())); + OpenPgpMetadata metadata = processSignedData(verifier); + assertTrue(metadata.containsVerifiedSignatureFrom(TestKeys.getEmilPublicKeyRing())); } private static PGPSignature generateV3Signature() throws IOException, PGPException { @@ -61,9 +61,9 @@ class VerifyVersion3SignaturePacketTest { return signatureGenerator.generate(); } - private MessageMetadata processSignedData(DecryptionStream verifier) throws IOException { + private OpenPgpMetadata processSignedData(DecryptionStream verifier) throws IOException { Streams.drain(verifier); verifier.close(); - return verifier.getMetadata(); + return verifier.getResult(); } } 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 0d58e7dd..2821ca89 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,6 +14,7 @@ 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; @@ -62,8 +63,8 @@ public class VerifyWithMissingPublicKeyCallbackTest { .addVerificationCert(unrelatedKeys) .setMissingCertificateCallback(new MissingPublicKeyCallback() { @Override - public PGPPublicKeyRing onMissingPublicKeyEncountered(long keyId) { - assertEquals(signingKey.getKeyID(), keyId, "Signing key-ID mismatch."); + public PGPPublicKeyRing onMissingPublicKeyEncountered(@Nonnull Long keyId) { + assertEquals(signingKey.getKeyID(), (long) keyId, "Signing key-ID mismatch."); return signingPubKeys; } })); @@ -73,7 +74,7 @@ public class VerifyWithMissingPublicKeyCallbackTest { verificationStream.close(); assertArrayEquals(msg.getBytes(StandardCharsets.UTF_8), plainOut.toByteArray()); - MessageMetadata metadata = verificationStream.getMetadata(); - assertTrue(metadata.isVerifiedSignedBy(signingPubKeys)); + OpenPgpMetadata metadata = verificationStream.getResult(); + assertTrue(metadata.containsVerifiedSignatureFrom(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 f3336373..b3ecabc8 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(); - MessageMetadata metadata = decryptionStream.getMetadata(); + OpenPgpMetadata metadata = decryptionStream.getResult(); if (expectSuccessfulVerification) { - assertTrue(metadata.isVerifiedSigned()); + assertTrue(metadata.isVerified()); } else { - assertFalse(metadata.isVerifiedSigned()); + assertFalse(metadata.isVerified()); } } 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 10f8dceb..d0486a3e 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.LITERAL_DATA); - check.next(InputSymbol.END_OF_SEQUENCE); + check.next(InputSymbol.LiteralData); + check.next(InputSymbol.EndOfSequence); assertTrue(check.isValid()); } @@ -35,10 +35,10 @@ public class PDATest { @Test public void testSimpleOpsSignedMesssageIsValid() throws MalformedOpenPgpMessageException { PDA check = new PDA(); - check.next(InputSymbol.ONE_PASS_SIGNATURE); - check.next(InputSymbol.LITERAL_DATA); - check.next(InputSymbol.SIGNATURE); - check.next(InputSymbol.END_OF_SEQUENCE); + check.next(InputSymbol.OnePassSignature); + check.next(InputSymbol.LiteralData); + check.next(InputSymbol.Signature); + check.next(InputSymbol.EndOfSequence); 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.LITERAL_DATA); - check.next(InputSymbol.END_OF_SEQUENCE); + check.next(InputSymbol.Signature); + check.next(InputSymbol.LiteralData); + check.next(InputSymbol.EndOfSequence); assertTrue(check.isValid()); } @@ -68,11 +68,11 @@ public class PDATest { @Test public void testOPSSignedCompressedMessageIsValid() throws MalformedOpenPgpMessageException { PDA check = new PDA(); - check.next(InputSymbol.ONE_PASS_SIGNATURE); - check.next(InputSymbol.COMPRESSED_DATA); + check.next(InputSymbol.OnePassSignature); + check.next(InputSymbol.CompressedData); // Here would be a nested PDA for the LiteralData packet - check.next(InputSymbol.SIGNATURE); - check.next(InputSymbol.END_OF_SEQUENCE); + check.next(InputSymbol.Signature); + check.next(InputSymbol.EndOfSequence); assertTrue(check.isValid()); } @@ -80,105 +80,105 @@ public class PDATest { @Test public void testOPSSignedEncryptedMessageIsValid() { PDA check = new PDA(); - check.next(InputSymbol.ONE_PASS_SIGNATURE); - check.next(InputSymbol.ENCRYPTED_DATA); - check.next(InputSymbol.SIGNATURE); - check.next(InputSymbol.END_OF_SEQUENCE); + check.next(InputSymbol.OnePassSignature); + check.next(InputSymbol.EncryptedData); + check.next(InputSymbol.Signature); + check.next(InputSymbol.EndOfSequence); assertTrue(check.isValid()); } @Test public void anyInputAfterEOSIsNotValid() { PDA check = new PDA(); - check.next(InputSymbol.LITERAL_DATA); - check.next(InputSymbol.END_OF_SEQUENCE); + check.next(InputSymbol.LiteralData); + check.next(InputSymbol.EndOfSequence); assertThrows(MalformedOpenPgpMessageException.class, - () -> check.next(InputSymbol.SIGNATURE)); + () -> check.next(InputSymbol.Signature)); } @Test public void testEncryptedMessageWithAppendedStandaloneSigIsNotValid() { PDA check = new PDA(); - check.next(InputSymbol.ENCRYPTED_DATA); + check.next(InputSymbol.EncryptedData); assertThrows(MalformedOpenPgpMessageException.class, - () -> check.next(InputSymbol.SIGNATURE)); + () -> check.next(InputSymbol.Signature)); } @Test public void testOPSSignedEncryptedMessageWithMissingSigIsNotValid() { PDA check = new PDA(); - check.next(InputSymbol.ONE_PASS_SIGNATURE); - check.next(InputSymbol.ENCRYPTED_DATA); + check.next(InputSymbol.OnePassSignature); + check.next(InputSymbol.EncryptedData); assertThrows(MalformedOpenPgpMessageException.class, - () -> check.next(InputSymbol.END_OF_SEQUENCE)); + () -> check.next(InputSymbol.EndOfSequence)); } @Test public void testTwoLiteralDataIsNotValid() { PDA check = new PDA(); - check.next(InputSymbol.LITERAL_DATA); + check.next(InputSymbol.LiteralData); assertThrows(MalformedOpenPgpMessageException.class, - () -> check.next(InputSymbol.LITERAL_DATA)); + () -> check.next(InputSymbol.LiteralData)); } @Test public void testTrailingSigIsNotValid() { PDA check = new PDA(); - check.next(InputSymbol.LITERAL_DATA); + check.next(InputSymbol.LiteralData); assertThrows(MalformedOpenPgpMessageException.class, - () -> check.next(InputSymbol.SIGNATURE)); + () -> check.next(InputSymbol.Signature)); } @Test public void testOPSAloneIsNotValid() { PDA check = new PDA(); - check.next(InputSymbol.ONE_PASS_SIGNATURE); + check.next(InputSymbol.OnePassSignature); assertThrows(MalformedOpenPgpMessageException.class, - () -> check.next(InputSymbol.END_OF_SEQUENCE)); + () -> check.next(InputSymbol.EndOfSequence)); } @Test public void testOPSLitWithMissingSigIsNotValid() { PDA check = new PDA(); - check.next(InputSymbol.ONE_PASS_SIGNATURE); - check.next(InputSymbol.LITERAL_DATA); + check.next(InputSymbol.OnePassSignature); + check.next(InputSymbol.LiteralData); assertThrows(MalformedOpenPgpMessageException.class, - () -> check.next(InputSymbol.END_OF_SEQUENCE)); + () -> check.next(InputSymbol.EndOfSequence)); } @Test public void testCompressedMessageWithStandalongAppendedSigIsNotValid() { PDA check = new PDA(); - check.next(InputSymbol.COMPRESSED_DATA); + check.next(InputSymbol.CompressedData); assertThrows(MalformedOpenPgpMessageException.class, - () -> check.next(InputSymbol.SIGNATURE)); + () -> check.next(InputSymbol.Signature)); } @Test public void testOPSCompressedDataWithMissingSigIsNotValid() { PDA check = new PDA(); - check.next(InputSymbol.ONE_PASS_SIGNATURE); - check.next(InputSymbol.COMPRESSED_DATA); + check.next(InputSymbol.OnePassSignature); + check.next(InputSymbol.CompressedData); assertThrows(MalformedOpenPgpMessageException.class, - () -> check.next(InputSymbol.END_OF_SEQUENCE)); + () -> check.next(InputSymbol.EndOfSequence)); } @Test public void testCompressedMessageFollowedByTrailingLiteralDataIsNotValid() { PDA check = new PDA(); - check.next(InputSymbol.COMPRESSED_DATA); + check.next(InputSymbol.CompressedData); assertThrows(MalformedOpenPgpMessageException.class, - () -> check.next(InputSymbol.LITERAL_DATA)); + () -> check.next(InputSymbol.LiteralData)); } @Test public void testOPSWithPrependedSigIsValid() { PDA check = new PDA(); - check.next(InputSymbol.SIGNATURE); - check.next(InputSymbol.ONE_PASS_SIGNATURE); - check.next(InputSymbol.LITERAL_DATA); - check.next(InputSymbol.SIGNATURE); - check.next(InputSymbol.END_OF_SEQUENCE); + check.next(InputSymbol.Signature); + check.next(InputSymbol.OnePassSignature); + check.next(InputSymbol.LiteralData); + check.next(InputSymbol.Signature); + check.next(InputSymbol.EndOfSequence); assertTrue(check.isValid()); } @@ -186,11 +186,11 @@ public class PDATest { @Test public void testPrependedSigInsideOPSSignedMessageIsValid() { PDA check = new PDA(); - check.next(InputSymbol.ONE_PASS_SIGNATURE); - check.next(InputSymbol.SIGNATURE); - check.next(InputSymbol.LITERAL_DATA); - check.next(InputSymbol.SIGNATURE); - check.next(InputSymbol.END_OF_SEQUENCE); + check.next(InputSymbol.OnePassSignature); + check.next(InputSymbol.Signature); + check.next(InputSymbol.LiteralData); + check.next(InputSymbol.Signature); + check.next(InputSymbol.EndOfSequence); 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 3e620386..216d0c65 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.MessageMetadata; +import org.pgpainless.decryption_verification.OpenPgpMetadata; import org.pgpainless.exception.KeyException; import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.key.TestKeys; @@ -185,10 +185,11 @@ public class EncryptDecryptTest { decryptor.close(); assertArrayEquals(secretMessage, decryptedSecretMessage.toByteArray()); - MessageMetadata result = decryptor.getMetadata(); - assertTrue(result.isVerifiedSignedBy(senderPub)); + OpenPgpMetadata result = decryptor.getResult(); + assertTrue(result.containsVerifiedSignatureFrom(senderPub)); + assertTrue(result.isSigned()); assertTrue(result.isEncrypted()); - assertTrue(result.isVerifiedSigned()); + assertTrue(result.isVerified()); } @TestTemplate @@ -232,7 +233,7 @@ public class EncryptDecryptTest { Streams.pipeAll(verifier, dummyOut); verifier.close(); - MessageMetadata decryptionResult = verifier.getMetadata(); + OpenPgpMetadata decryptionResult = verifier.getResult(); assertFalse(decryptionResult.getVerifiedSignatures().isEmpty()); } @@ -262,7 +263,7 @@ public class EncryptDecryptTest { Streams.pipeAll(verifier, signOut); verifier.close(); - MessageMetadata metadata = verifier.getMetadata(); + OpenPgpMetadata metadata = verifier.getResult(); 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 5f6c6c15..7d2fa453 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,7 +11,6 @@ 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; @@ -23,7 +22,6 @@ 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; @@ -33,8 +31,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_legacy.EdDSALegacyCurve; -import org.pgpainless.key.generation.type.xdh_legacy.XDHLegacySpec; +import org.pgpainless.key.generation.type.eddsa.EdDSACurve; +import org.pgpainless.key.generation.type.xdh.XDHSpec; import org.pgpainless.key.util.KeyRingUtils; import org.pgpainless.util.Passphrase; @@ -51,11 +49,11 @@ public class EncryptionOptionsTest { @BeforeAll public static void generateKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { secretKeys = PGPainless.buildKeyRing() - .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER) + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER) .build()) - .addSubkey(KeySpec.getBuilder(KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.ENCRYPT_COMMS) + .addSubkey(KeySpec.getBuilder(KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_COMMS) .build()) - .addSubkey(KeySpec.getBuilder(KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.ENCRYPT_STORAGE) + .addSubkey(KeySpec.getBuilder(KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_STORAGE) .build()) .addUserId("test@pgpainless.org") .build(); @@ -124,21 +122,21 @@ public class EncryptionOptionsTest { EncryptionOptions options = new EncryptionOptions(); assertThrows(IllegalArgumentException.class, () -> options.addRecipients(Collections.emptyList())); assertThrows(IllegalArgumentException.class, () -> options.addRecipients(Collections.emptyList(), - ArrayList::new)); + encryptionCapableKeys -> encryptionCapableKeys)); } @Test public void testAddEmptyPassphraseFails() { EncryptionOptions options = new EncryptionOptions(); assertThrows(IllegalArgumentException.class, () -> - options.addMessagePassphrase(Passphrase.emptyPassphrase())); + options.addPassphrase(Passphrase.emptyPassphrase())); } @Test public void testAddRecipient_KeyWithoutEncryptionKeyFails() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { EncryptionOptions options = new EncryptionOptions(); PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() - .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA)) + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA)) .addUserId("test@pgpainless.org") .build(); PGPPublicKeyRing publicKeys = KeyRingUtils.publicKeyRingFrom(secretKeys); @@ -152,9 +150,8 @@ public class EncryptionOptionsTest { assertThrows(KeyException.UnacceptableEncryptionKeyException.class, () -> options.addRecipient(publicKeys, new EncryptionOptions.EncryptionKeySelector() { - @NotNull @Override - public List selectEncryptionSubkeys(@NotNull List encryptionCapableKeys) { + public List selectEncryptionSubkeys(@Nonnull List encryptionCapableKeys) { return Collections.emptyList(); } })); @@ -162,7 +159,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 ae640661..4ac43630 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() - .addMessagePassphrase(Passphrase.fromPassword("dummy")))); + .addPassphrase(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 59ad4596..242b430b 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() - .addMessagePassphrase(Passphrase.fromPassword("sw0rdf1sh"))) + .addPassphrase(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 28097db6..5304c32c 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_legacy.EdDSALegacyCurve; +import org.pgpainless.key.generation.type.eddsa.EdDSACurve; import org.pgpainless.key.generation.type.rsa.RsaLength; -import org.pgpainless.key.generation.type.xdh_legacy.XDHLegacySpec; +import org.pgpainless.key.generation.type.xdh.XDHSpec; 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_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA)) - .addSubkey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.SIGN_DATA)) + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA)) + .addSubkey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.SIGN_DATA)) .addSubkey(KeySpec.getBuilder(KeyType.RSA(RsaLength._3072), KeyFlag.SIGN_DATA)) - .addSubkey(KeySpec.getBuilder(KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)) + .addSubkey(KeySpec.getBuilder(KeyType.XDH(XDHSpec._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 c62116b3..156e6b57 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.MessageMetadata; +import org.pgpainless.decryption_verification.OpenPgpMetadata; 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_legacy.EdDSALegacyCurve; +import org.pgpainless.key.generation.type.eddsa.EdDSACurve; import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.util.KeyRingUtils; @@ -106,11 +106,12 @@ public class SigningTest { Streams.pipeAll(decryptionStream, plaintextOut); decryptionStream.close(); - MessageMetadata metadata = decryptionStream.getMetadata(); + OpenPgpMetadata metadata = decryptionStream.getResult(); assertTrue(metadata.isEncrypted()); - assertTrue(metadata.isVerifiedSigned()); - assertTrue(metadata.isVerifiedSignedBy(KeyRingUtils.publicKeyRingFrom(cryptieKeys))); - assertFalse(metadata.isVerifiedSignedBy(julietKeys)); + assertTrue(metadata.isSigned()); + assertTrue(metadata.isVerified()); + assertTrue(metadata.containsVerifiedSignatureFrom(KeyRingUtils.publicKeyRingFrom(cryptieKeys))); + assertFalse(metadata.containsVerifiedSignatureFrom(julietKeys)); } @TestTemplate @@ -187,7 +188,7 @@ public class SigningTest { throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() .setPrimaryKey(KeySpec.getBuilder( - KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA) + KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA) .overridePreferredHashAlgorithms()) .addUserId("Alice") .build(); @@ -207,7 +208,7 @@ public class SigningTest { SubkeyIdentifier signingKey = sigs.keySet().iterator().next(); PGPSignature signature = sigs.get(signingKey).iterator().next(); - assertEquals(PGPainless.getPolicy().getDataSignatureHashAlgorithmPolicy().defaultHashAlgorithm().getAlgorithmId(), + assertEquals(PGPainless.getPolicy().getSignatureHashAlgorithmPolicy().defaultHashAlgorithm().getAlgorithmId(), signature.getHashAlgorithm()); } @@ -217,7 +218,7 @@ public class SigningTest { throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() .setPrimaryKey( - KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA) + KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA) .overridePreferredHashAlgorithms(HashAlgorithm.MD5)) .addUserId("Alice") .build(); @@ -237,7 +238,7 @@ public class SigningTest { SubkeyIdentifier signingKey = sigs.keySet().iterator().next(); PGPSignature signature = sigs.get(signingKey).iterator().next(); - assertEquals(PGPainless.getPolicy().getDataSignatureHashAlgorithmPolicy().defaultHashAlgorithm().getAlgorithmId(), + assertEquals(PGPainless.getPolicy().getSignatureHashAlgorithmPolicy().defaultHashAlgorithm().getAlgorithmId(), signature.getHashAlgorithm()); } @@ -246,7 +247,7 @@ public class SigningTest { public void signingWithNonCapableKeyThrowsKeyCannotSignException() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() - .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER)) + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER)) .addUserId("Alice") .build(); @@ -262,7 +263,7 @@ public class SigningTest { public void signWithInvalidUserIdThrowsKeyValidationError() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() - .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._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 d97891d8..ba832516 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() - .addMessagePassphrase(Passphrase.fromPassword("p4ssphr4s3")) + .addPassphrase(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().addMessagePassphrase(Passphrase.fromPassword("p4ssphr4s3"))); + .withOptions(new ConsumerOptions().addDecryptionPassphrase(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 d6bcb0b1..dc9c51f2 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_legacy.EdDSALegacyCurve; +import org.pgpainless.key.generation.type.eddsa.EdDSACurve; 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_LEGACY.getAlgorithmId(), + assertEquals(PublicKeyAlgorithm.EDDSA.getAlgorithmId(), keyInfo.getPublicKey().getAlgorithm()); - assertEquals(PublicKeyAlgorithm.EDDSA_LEGACY.getAlgorithmId(), + assertEquals(PublicKeyAlgorithm.EDDSA.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_LEGACY(EdDSALegacyCurve._Ed25519), + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._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 3b29e35d..858c99a9 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/example/ManagePolicy.java +++ b/pgpainless-core/src/test/java/org/pgpainless/example/ManagePolicy.java @@ -43,10 +43,7 @@ public class ManagePolicy { @AfterEach public void resetPolicy() { // Policy for hash algorithms in non-revocation signatures - PGPainless.getPolicy().setCertificationSignatureHashAlgorithmPolicy( - Policy.HashAlgorithmPolicy.static2022SignatureHashAlgorithmPolicy()); - // Policy for hash algorithms in data signatures - PGPainless.getPolicy().setDataSignatureHashAlgorithmPolicy( + PGPainless.getPolicy().setSignatureHashAlgorithmPolicy( Policy.HashAlgorithmPolicy.static2022SignatureHashAlgorithmPolicy()); // Policy for hash algorithms in revocation signatures PGPainless.getPolicy().setRevocationSignatureHashAlgorithmPolicy( @@ -86,7 +83,7 @@ public class ManagePolicy { // Get PGPainless' policy singleton Policy policy = PGPainless.getPolicy(); - Policy.HashAlgorithmPolicy sigHashAlgoPolicy = policy.getDataSignatureHashAlgorithmPolicy(); + Policy.HashAlgorithmPolicy sigHashAlgoPolicy = policy.getSignatureHashAlgorithmPolicy(); assertTrue(sigHashAlgoPolicy.isAcceptable(HashAlgorithm.SHA512)); // Per default, non-revocation signatures using SHA-1 are rejected assertFalse(sigHashAlgoPolicy.isAcceptable(HashAlgorithm.SHA1)); @@ -98,9 +95,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.setDataSignatureHashAlgorithmPolicy(customPolicy); + policy.setSignatureHashAlgorithmPolicy(customPolicy); - sigHashAlgoPolicy = policy.getDataSignatureHashAlgorithmPolicy(); + sigHashAlgoPolicy = policy.getSignatureHashAlgorithmPolicy(); 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/KeyWithUnsupportedSignatureSubpacketTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/KeyWithUnsupportedSignatureSubpacketTest.java new file mode 100644 index 00000000..82cb8d2a --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/key/KeyWithUnsupportedSignatureSubpacketTest.java @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.key.protection.SecretKeyRingProtector; + +import java.io.IOException; +import java.util.Date; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class KeyWithUnsupportedSignatureSubpacketTest { + + @Test + public void testCanSetExpirationDateOnKeyContainingUnknownSubpacket34() throws IOException, PGPException { + String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "\n" + + "lFgEZWiyNhYJKwYBBAHaRw8BAQdA71QipJ0CAqOEqQWjuoQE4E7LarKSrNDwE/6K\n" + + "bQNrCLwAAQCtJ8kVG2AmbDfdVtr/7Ag+yBh0oCvjRvyUCOyIbruOeg+6tClTdWJw\n" + + "YWNrZXQzNCBUZXN0S2V5IDx0ZXN0QHBncGFpbmxlc3Mub3JnPoiTBBMWCgA7FiEE\n" + + "zhy5yrnZYU/iBza4G03SQVuWqx0FAmVosjYCGwMFCwkIBwICIgIGFQoJCAsCBBYC\n" + + "AwECHgcCF4AACgkQG03SQVuWqx1UGgD+IYLeh9t5eJCEnzueuOTYnTnrzyhnLgm9\n" + + "dw5qwMXU8VQA/28GCOb7610hyjiBbrrcshkWAKuMwp8bUSz5FOeS5cQEnF0EZWiy\n" + + "NhIKKwYBBAGXVQEFAQEHQK99ClLDYtn0I2b6Y26NhaL0RWcrNoI/ci0xgXEK2L0Y\n" + + "AwEIBwAA/06qciQHI0v7MP2LMWm/ZuTJwzlPqV8VsBhrDMyUPUD4D52IeAQYFgoA\n" + + "IBYhBM4cucq52WFP4gc2uBtN0kFblqsdBQJlaLI2AhsMAAoJEBtN0kFblqsdRQ0A\n" + + "/iUJ/Fp+D2RjZL+aiwByIxPCVvMJ7a28+GQGjg3hsU2BAP474dfOOVZiTDLWWxsB\n" + + "wxfzOAQxXDhgR9xd/Lk3MNJxDg==\n" + + "=YAt0\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); + assertNotNull(secretKeys); + PGPainless.modifyKeyRing(secretKeys) + .setExpirationDate(new Date(), SecretKeyRingProtector.unprotectedKeys()) + .done(); + } +} 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 43a3dfba..d61aeedd 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/OpenPgpV4FingerprintTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/OpenPgpV4FingerprintTest.java @@ -4,7 +4,6 @@ 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; @@ -113,7 +112,6 @@ public class OpenPgpV4FingerprintTest { OpenPgpFingerprint fingerprint = OpenPgpFingerprint.parseFromBinary(binary); assertTrue(fingerprint instanceof OpenPgpV4Fingerprint); assertEquals(hex, fingerprint.toString()); - assertArrayEquals(binary, fingerprint.getBytes()); } @Test @@ -124,7 +122,6 @@ 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 add18fd5..fe792e4d 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/SubkeyIdentifierTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/SubkeyIdentifierTest.java @@ -5,10 +5,8 @@ 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; @@ -104,15 +102,6 @@ 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 1290ad9c..93cb6922 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java @@ -197,14 +197,15 @@ public class UserIdTest { } @Test - public void toStringTest() { - UserId id = UserId.builder() + public void asStringTest() { + UserId id = UserId.newBuilder() .withName("Alice") .withComment("Work Email") .withEmail("alice@pgpainless.org") .build(); - assertEquals(id.toString(), id.toString()); + // noinspection deprecation + assertEquals(id.toString(), id.asString()); } @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 fd5530ba..89b0fccf 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 77023908..969a0587 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_legacy.EdDSALegacyCurve; +import org.pgpainless.key.generation.type.eddsa.EdDSACurve; import org.pgpainless.key.generation.type.rsa.RsaLength; -import org.pgpainless.key.generation.type.xdh_legacy.XDHLegacySpec; +import org.pgpainless.key.generation.type.xdh.XDHSpec; 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_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.SIGN_DATA)) + .addSubkey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.SIGN_DATA)) .addSubkey(KeySpec.getBuilder( - KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)) + KeyType.XDH(XDHSpec._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(EdDSALegacyCurve._Ed25519.getName(), eddsaInfo.getCurveName()); + assertEquals(EdDSACurve._Ed25519.getName(), eddsaInfo.getCurveName()); assertEquals(256, eddsaSub.getPublicKey().getBitStrength()); PGPSecretKey xdhSub = iterator.next(); KeyInfo xdhInfo = new KeyInfo(xdhSub); - assertEquals(XDHLegacySpec._X25519.getCurveName(), xdhInfo.getCurveName()); + assertEquals(XDHSpec._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 02d5aed0..7b6710c5 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_legacy.XDHLegacySpec; +import org.pgpainless.key.generation.type.xdh.XDHSpec; 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_LEGACY(XDHLegacySpec._X25519) + KeyType.XDH(XDHSpec._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 4cb992db..8ea4877d 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_legacy.EdDSALegacyCurve; -import org.pgpainless.key.generation.type.xdh_legacy.XDHLegacySpec; +import org.pgpainless.key.generation.type.eddsa.EdDSACurve; +import org.pgpainless.key.generation.type.xdh.XDHSpec; 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_LEGACY(EdDSALegacyCurve._Ed25519), + KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA)) - .addSubkey(KeySpec.getBuilder(KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.ENCRYPT_COMMS)) + .addSubkey(KeySpec.getBuilder(KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_COMMS)) .addUserId(UserId.onlyEmail("alice@wonderland.lit").toString()) .build(); - assertEquals(PublicKeyAlgorithm.EDDSA_LEGACY.getAlgorithmId(), keyRing.getPublicKey().getAlgorithm()); + assertEquals(PublicKeyAlgorithm.EDDSA.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 0ad564db..d2697b82 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_legacy.EdDSALegacyCurve; -import org.pgpainless.key.generation.type.xdh_legacy.XDHLegacySpec; +import org.pgpainless.key.generation.type.eddsa.EdDSACurve; +import org.pgpainless.key.generation.type.xdh.XDHSpec; 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_LEGACY(XDHLegacySpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)) - .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA) + .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) .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 deleted file mode 100644 index e477aeef..00000000 --- a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithoutPrimaryKeyFlagsTest.java +++ /dev/null @@ -1,93 +0,0 @@ -// 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 e6a5c96a..24484cd0 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.MessageMetadata; +import org.pgpainless.decryption_verification.OpenPgpMetadata; 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_legacy.EdDSALegacyCurve; -import org.pgpainless.key.generation.type.xdh_legacy.XDHLegacySpec; +import org.pgpainless.key.generation.type.eddsa.EdDSACurve; +import org.pgpainless.key.generation.type.xdh.XDHSpec; 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_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)) + .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)) .setExpirationDate(expirationDate) .build(); @@ -88,10 +88,10 @@ public class GenerateKeyWithoutUserIdTest { Streams.pipeAll(decryptionStream, plaintextOut); decryptionStream.close(); - MessageMetadata metadata = decryptionStream.getMetadata(); + OpenPgpMetadata metadata = decryptionStream.getResult(); - assertTrue(metadata.isVerifiedSignedBy(certificate), - failuresToString(metadata.getRejectedInlineSignatures())); + assertTrue(metadata.containsVerifiedSignatureFrom(certificate), + failuresToString(metadata.getInvalidInbandSignatures())); 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 a00e58c7..24ea4aa4 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_legacy.EdDSALegacyCurve; -import org.pgpainless.key.generation.type.xdh_legacy.XDHLegacySpec; +import org.pgpainless.key.generation.type.eddsa.EdDSACurve; +import org.pgpainless.key.generation.type.xdh.XDHSpec; 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_LEGACY(XDHLegacySpec._X25519), KeyFlag.SIGN_DATA)); + KeyType.XDH(XDHSpec._X25519), KeyFlag.SIGN_DATA)); assertThrows(IllegalArgumentException.class, () -> KeySpec.getBuilder( - KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.CERTIFY_OTHER)); + KeyType.XDH(XDHSpec._X25519), KeyFlag.CERTIFY_OTHER)); assertThrows(IllegalArgumentException.class, () -> KeySpec.getBuilder( - KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.AUTHENTICATION)); + KeyType.XDH(XDHSpec._X25519), KeyFlag.AUTHENTICATION)); assertThrows(IllegalArgumentException.class, () -> KeySpec.getBuilder( - KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.ENCRYPT_COMMS)); + KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.ENCRYPT_COMMS)); assertThrows(IllegalArgumentException.class, () -> KeySpec.getBuilder( - KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.ENCRYPT_STORAGE)); + KeyType.EDDSA(EdDSACurve._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 238d1a40..231b0485 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_legacy.EdDSALegacyCurve; -import org.pgpainless.key.generation.type.xdh_legacy.XDHLegacySpec; +import org.pgpainless.key.generation.type.eddsa.EdDSACurve; +import org.pgpainless.key.generation.type.xdh.XDHSpec; 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_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.SIGN_DATA).build(), + .addSubKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._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_LEGACY(XDHLegacySpec._X25519), KeyFlag.ENCRYPT_COMMS).build(), + .addSubKey(KeySpec.getBuilder(KeyType.XDH(XDHSpec._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 34465bba..d62d5bad 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_legacy.EdDSALegacyCurve; +import org.pgpainless.key.generation.type.eddsa.EdDSACurve; 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(Passphrase.emptyPassphrase()) + .changePassphraseFromOldPassphrase(null) .withSecureDefaultSettings() .toNewPassphrase(Passphrase.fromPassword("sw0rdf1sh")) .done(); @@ -224,7 +224,7 @@ public class KeyRingInfoTest { PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() .setPrimaryKey(KeySpec.getBuilder( - KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER)) + KeyType.EDDSA(EdDSACurve._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_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER)) + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._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 abb067d1..0caf8b75 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_legacy.EdDSALegacyCurve; -import org.pgpainless.key.generation.type.xdh_legacy.XDHLegacySpec; +import org.pgpainless.key.generation.type.eddsa.EdDSACurve; +import org.pgpainless.key.generation.type.xdh.XDHSpec; 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_LEGACY(EdDSALegacyCurve._Ed25519), + KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.SIGN_DATA, KeyFlag.CERTIFY_OTHER)) .addSubkey(KeySpec.getBuilder( - KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.ENCRYPT_COMMS)) + KeyType.XDH(XDHSpec._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_LEGACY(EdDSALegacyCurve._Ed25519), + KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.SIGN_DATA, KeyFlag.CERTIFY_OTHER)) - .addSubkey(KeySpec.getBuilder(KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.ENCRYPT_COMMS)) + .addSubkey(KeySpec.getBuilder(KeyType.XDH(XDHSpec._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 85cddfd6..1eb086fd 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_legacy.EdDSALegacyCurve; +import org.pgpainless.key.generation.type.eddsa.EdDSACurve; 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_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.SIGN_DATA).build()); + KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._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 7e15c998..19f84930 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,11 +12,9 @@ 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; @@ -115,17 +113,16 @@ 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, DateExtensionsKt.plusSeconds(now, 1)) + secretKeys = PGPainless.modifyKeyRing(secretKeys) .addPrimaryUserId(bob, SecretKeyRingProtector.unprotectedKeys()) .done(); - assertEquals("Bob", PGPainless.inspectKeyRing(secretKeys, DateExtensionsKt.plusSeconds(now, 2)).getPrimaryUserId()); + assertEquals("Bob", PGPainless.inspectKeyRing(secretKeys).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 index e1926b67..c550e376 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/modification/ChangeSubkeyExpirationTimeTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/modification/ChangeSubkeyExpirationTimeTest.java @@ -4,6 +4,15 @@ package org.pgpainless.key.modification; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.util.Date; + +import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.junit.JUtils; @@ -15,16 +24,10 @@ 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() { + public void changeExpirationTimeOfSubkey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().modernKeyRing("Alice"); Date now = secretKeys.getPublicKey().getCreationTime(); Date inAnHour = new Date(now.getTime() + 1000 * 60 * 60); @@ -42,7 +45,7 @@ public class ChangeSubkeyExpirationTimeTest { } @Test - public void changeExpirationTimeOfExpiredSubkey() throws IOException { + public void changeExpirationTimeOfExpiredSubkey() throws PGPException, IOException { PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing( "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + "Version: PGPainless\n" + 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 04197d6f..e129e4be 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,11 +67,12 @@ 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_LEGACY, 250); + 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); 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 370cfd85..45aaf20f 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 == TestKeys.CRYPTIE_KEY_ID) { + if (keyId.equals(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 == TestKeys.CRYPTIE_KEY_ID; + return keyId.equals(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 ba6673e5..f324892f 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(Passphrase.emptyPassphrase()) + .changePassphraseFromOldPassphrase(null) .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 deleted file mode 100644 index f85bd43f..00000000 --- a/pgpainless-core/src/test/java/org/pgpainless/key/util/KeyIdUtilTest.java +++ /dev/null @@ -1,41 +0,0 @@ -// 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 6e90847d..31092c28 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/policy/PolicySetterTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/policy/PolicySetterTest.java @@ -16,15 +16,9 @@ import org.pgpainless.algorithm.PublicKeyAlgorithm; public class PolicySetterTest { @Test - public void testSetCertificationSignatureHashAlgorithmPolicy_NullFails() { + public void testSetSignatureHashAlgorithmPolicy_NullFails() { Policy policy = Policy.getInstance(); - assertThrows(NullPointerException.class, () -> policy.setCertificationSignatureHashAlgorithmPolicy(null)); - } - - @Test - public void testSetDataSignatureHashAlgorithmPolicy_NullFails() { - Policy policy = Policy.getInstance(); - assertThrows(NullPointerException.class, () -> policy.setDataSignatureHashAlgorithmPolicy(null)); + assertThrows(NullPointerException.class, () -> policy.setSignatureHashAlgorithmPolicy(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 9ff4df85..aa7078e4 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.setCertificationSignatureHashAlgorithmPolicy(new Policy.HashAlgorithmPolicy(HashAlgorithm.SHA512, sigHashAlgoMap)); + policy.setSignatureHashAlgorithmPolicy(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.getCertificationSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA512)); - assertTrue(policy.getCertificationSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA512.getAlgorithmId())); + assertTrue(policy.getSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA512)); + assertTrue(policy.getSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA512.getAlgorithmId())); // Usage date before termination date -> acceptable - assertTrue(policy.getCertificationSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA1, DateUtil.parseUTCDate("2000-01-01 00:00:00 UTC"))); - assertTrue(policy.getCertificationSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA1.getAlgorithmId(), DateUtil.parseUTCDate("2000-01-01 00:00:00 UTC"))); + 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"))); } @Test public void testUnacceptableSignatureHashAlgorithm() { - 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"))); + 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"))); } @Test public void testDefaultSignatureHashAlgorithm() { - assertEquals(HashAlgorithm.SHA512, policy.getCertificationSignatureHashAlgorithmPolicy().defaultHashAlgorithm()); + assertEquals(HashAlgorithm.SHA512, policy.getSignatureHashAlgorithmPolicy().defaultHashAlgorithm()); } @Test public void testAcceptableRevocationSignatureHashAlgorithm() { assertTrue(policy.getRevocationSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA384)); assertTrue(policy.getRevocationSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA384.getAlgorithmId())); - assertTrue(policy.getCertificationSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA1, DateUtil.parseUTCDate("2000-01-01 00:00:00 UTC"))); - assertTrue(policy.getCertificationSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA1.getAlgorithmId(), DateUtil.parseUTCDate("2000-01-01 00:00:00 UTC"))); + 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"))); } @Test public void testUnacceptableRevocationSignatureHashAlgorithm() { assertFalse(policy.getRevocationSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.RIPEMD160)); assertFalse(policy.getRevocationSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.RIPEMD160.getAlgorithmId())); - assertFalse(policy.getCertificationSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA1, DateUtil.parseUTCDate("2020-01-01 00:00:00 UTC"))); - assertFalse(policy.getCertificationSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA1.getAlgorithmId(), DateUtil.parseUTCDate("2020-01-01 00:00:00 UTC"))); + 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"))); } @Test @@ -181,8 +181,8 @@ public class PolicyTest { @Test public void testUnknownSignatureHashAlgorithmIsNotAcceptable() { - assertFalse(policy.getCertificationSignatureHashAlgorithmPolicy().isAcceptable(-1)); - assertFalse(policy.getCertificationSignatureHashAlgorithmPolicy().isAcceptable(-1, new Date())); + assertFalse(policy.getSignatureHashAlgorithmPolicy().isAcceptable(-1)); + assertFalse(policy.getSignatureHashAlgorithmPolicy().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 5489a11c..a859008a 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 getSecurityProvider() { + protected Provider _getProvider() { 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 ff26506d..6ece093b 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.MessageMetadata; +import org.pgpainless.decryption_verification.OpenPgpMetadata; import org.pgpainless.key.OpenPgpV4Fingerprint; import org.pgpainless.key.util.KeyRingUtils; @@ -154,8 +154,8 @@ public class IgnoreMarkerPacketsTest { Streams.pipeAll(decryptionStream, outputStream); decryptionStream.close(); - MessageMetadata metadata = decryptionStream.getMetadata(); - assertTrue(metadata.isVerifiedSignedBy(new OpenPgpV4Fingerprint("D1A66E1A23B182C9980F788CFBFCC82A015E7330"))); + OpenPgpMetadata metadata = decryptionStream.getResult(); + assertTrue(metadata.containsVerifiedSignatureFrom(new OpenPgpV4Fingerprint("D1A66E1A23B182C9980F788CFBFCC82A015E7330"))); } @Test @@ -204,8 +204,8 @@ public class IgnoreMarkerPacketsTest { decryptionStream.close(); assertArrayEquals(data.getBytes(StandardCharsets.UTF_8), outputStream.toByteArray()); - MessageMetadata metadata = decryptionStream.getMetadata(); - assertTrue(metadata.isVerifiedSignedBy(new OpenPgpV4Fingerprint("D1A66E1A23B182C9980F788CFBFCC82A015E7330"))); + OpenPgpMetadata metadata = decryptionStream.getResult(); + assertTrue(metadata.containsVerifiedSignatureFrom(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 fd7c53e9..0042d14b 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_LEGACY.getAlgorithmId(), encoded[5]); + assertEquals(PublicKeyAlgorithm.EDDSA.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 1caeb9e9..79149dd2 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.LIBREPGP_OCB_ENCRYPTED_DATA)); + hashed.setFeature(true, Feature.toBitmask(Feature.MODIFICATION_DETECTION, Feature.GNUPG_AEAD_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.LIBREPGP_OCB_ENCRYPTED_DATA)); - assertFalse(featureSet.contains(Feature.LIBREPGP_VERSION_5_PUBLIC_KEY)); + assertTrue(featureSet.contains(Feature.GNUPG_AEAD_ENCRYPTED_DATA)); + assertFalse(featureSet.contains(Feature.GNUPG_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 2b0f4d35..bf1cb694 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 56605f83..72acc125 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,6 +8,8 @@ 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; @@ -31,7 +33,7 @@ import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil; public class ThirdPartyDirectKeySignatureBuilderTest { @Test - public void testDirectKeySignatureBuilding() throws PGPException { + public void testDirectKeySignatureBuilding() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() .modernKeyRing("Alice"); @@ -53,7 +55,7 @@ public class ThirdPartyDirectKeySignatureBuilderTest { } }); - PGPSignature directKeySig = dsb.build(); + PGPSignature directKeySig = dsb.build(secretKeys.getPublicKey()); 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 14eed4de..beb87ec5 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.LIBREPGP_OCB_ENCRYPTED_DATA); + wrapper.setFeatures(Feature.MODIFICATION_DETECTION, Feature.GNUPG_AEAD_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.LIBREPGP_OCB_ENCRYPTED_DATA)); + subpackets.setFeature(true, Feature.toBitmask(Feature.MODIFICATION_DETECTION, Feature.GNUPG_AEAD_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 d0d37117..5132ef57 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() - .addMessagePassphrase(Passphrase.fromPassword("p1")) - .addMessagePassphrase(Passphrase.fromPassword("p2")) + .addPassphrase(Passphrase.fromPassword("p1")) + .addPassphrase(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() - .addMessagePassphrase(passphrase)); + .addDecryptionPassphrase(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 dbf7ca24..d3c503ab 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() - .addMessagePassphrase(encryptionPassphrase) + .addPassphrase(encryptionPassphrase) .addRecipient(encryptionKey) )); @@ -66,7 +66,7 @@ public class SymmetricEncryptionTest { DecryptionStream decryptor = PGPainless.decryptAndOrVerify() .onInputStream(new ByteArrayInputStream(ciphertext)) .withOptions(new ConsumerOptions() - .addMessagePassphrase(encryptionPassphrase)); + .addDecryptionPassphrase(encryptionPassphrase)); ByteArrayOutputStream decrypted = new ByteArrayOutputStream(); @@ -103,7 +103,7 @@ public class SymmetricEncryptionTest { EncryptionStream encryptor = PGPainless.encryptAndOrSign().onOutputStream(ciphertextOut) .withOptions(ProducerOptions.encrypt( EncryptionOptions.encryptCommunications() - .addMessagePassphrase(Passphrase.fromPassword("mellon")))); + .addPassphrase(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) - .addMessagePassphrase(Passphrase.fromPassword("meldir")))); + .addDecryptionPassphrase(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 95e3961b..147b957d 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/util/ArmoredInputStreamFactoryTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/util/ArmoredInputStreamFactoryTest.java @@ -11,6 +11,8 @@ 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 { @@ -28,4 +30,22 @@ 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/test/java/org/pgpainless/util/GuessPreferredHashAlgorithmTest.java b/pgpainless-core/src/test/java/org/pgpainless/util/GuessPreferredHashAlgorithmTest.java index e276ba8f..ca7a06d1 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_legacy.EdDSALegacyCurve; +import org.pgpainless.key.generation.type.eddsa.EdDSACurve; 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_LEGACY(EdDSALegacyCurve._Ed25519), + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._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 164befb1..98688a94 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/util/MultiMapTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/util/MultiMapTest.java @@ -41,37 +41,7 @@ public class MultiMapTest { assertTrue(multiMap.containsKey("alice")); assertTrue(multiMap.containsValue("wonderland")); assertNotNull(multiMap.get("alice")); - 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")); + assertTrue(multiMap.get("alice").contains("wonderland")); } @Test @@ -134,7 +104,7 @@ public class MultiMapTest { @Test public void emptyAfterClear() { MultiMap map = new MultiMap<>(); - map.plus("test", "foo"); + map.put("test", "foo"); assertFalse(map.isEmpty()); map.clear(); assertTrue(map.isEmpty()); @@ -143,8 +113,8 @@ public class MultiMapTest { @Test public void addTwoRemoveOneWorks() { MultiMap map = new MultiMap<>(); - map.plus("alice", "wonderland"); - map.plus("bob", "builder"); + map.put("alice", "wonderland"); + map.put("bob", "builder"); map.removeAll("alice"); assertFalse(map.containsKey("alice")); @@ -155,11 +125,11 @@ public class MultiMapTest { @Test public void addMultiValue() { MultiMap addOneByOne = new MultiMap<>(); - addOneByOne.plus("foo", "bar"); - addOneByOne.plus("foo", "baz"); + addOneByOne.put("foo", "bar"); + addOneByOne.put("foo", "baz"); MultiMap addOnce = new MultiMap<>(); - addOnce.plus("foo", new HashSet<>(Arrays.asList("baz", "bar"))); + addOnce.put("foo", new HashSet<>(Arrays.asList("baz", "bar"))); assertEquals(addOneByOne, addOnce); } @@ -168,7 +138,7 @@ public class MultiMapTest { public void addMultiValueRemoveSingle() { MultiMap map = new MultiMap<>(); map.put("foo", "bar"); - map.plus("foo", "baz"); + map.put("foo", "baz"); map.remove("foo", "bar"); assertFalse(map.isEmpty()); @@ -179,9 +149,9 @@ public class MultiMapTest { @Test public void addMultiValueRemoveAll() { MultiMap map = new MultiMap<>(); - map.plus("foo", "bar"); - map.plus("foo", "baz"); - map.plus("bingo", "bango"); + map.put("foo", "bar"); + map.put("foo", "baz"); + map.put("bingo", "bango"); map.removeAll("foo"); assertFalse(map.isEmpty()); @@ -190,23 +160,23 @@ public class MultiMapTest { } @Test - public void plusAll() { + public void putAll() { MultiMap map = new MultiMap<>(); - map.plus("A", "1"); - map.plus("A", "2"); - map.plus("B", "1"); + map.put("A", "1"); + map.put("A", "2"); + map.put("B", "1"); MultiMap other = new MultiMap<>(); - other.plus("A", "1"); - other.plus("B", "2"); - other.plus("C", "3"); + other.put("A", "1"); + other.put("B", "2"); + other.put("C", "3"); - map.plusAll(other); - assertTrue(map.contains("A", "1")); - assertTrue(map.contains("A", "2")); - assertTrue(map.contains("B", "1")); - assertTrue(map.contains("B", "2")); - assertTrue(map.contains("C", "3")); + 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")); } @Test @@ -218,9 +188,9 @@ public class MultiMapTest { @Test public void flattenMap() { MultiMap map = new MultiMap<>(); - map.plus("A", "1"); - map.plus("A", "2"); - map.plus("B", "1"); + map.put("A", "1"); + map.put("A", "2"); + map.put("B", "1"); Set expected = new LinkedHashSet<>(); expected.add("1"); 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 c7f6d722..bf4955cd 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.assertTrue; +import static org.junit.jupiter.api.Assertions.assertNull; 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.MultiMap; import org.pgpainless.util.selection.keyring.impl.ExactUserId; +import org.pgpainless.util.MultiMap; 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()); - assertTrue(selected.get("invalidId").isEmpty()); + assertNull(selected.get("invalidId")); } @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.plus(TestKeys.JULIET_UID, julietCollection); + map.put(TestKeys.JULIET_UID, julietCollection); PGPPublicKeyRingCollection emilCollection = new PGPPublicKeyRingCollection(Collections.singletonList(emil)); - map.plus(TestKeys.EMIL_UID, emilCollection); + map.put(TestKeys.EMIL_UID, emilCollection); assertEquals(2, julietCollection.size()); - map.plus("invalidId", emilCollection); + map.put("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()); - assertTrue(selected.get("invalidId").isEmpty()); + assertNull(selected.get("invalidId")); } } 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 99a0c87c..3a49247c 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,15 +4,6 @@ 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; @@ -20,10 +11,20 @@ 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 PGPException { + public void testSelectUserIds() throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() .simpleEcKeyRing(""); secretKeys = PGPainless.modifyKeyRing(secretKeys) @@ -33,53 +34,48 @@ public class SelectUserIdTest { SecretKeyRingProtector.unprotectedKeys()) .done(); - List userIds = PGPainless.inspectKeyRing(secretKeys).getValidUserIds(); - List validEmail = userIds.stream().filter(SelectUserId.and( + List validEmail = SelectUserId.and( SelectUserId.validUserId(secretKeys), SelectUserId.containsEmailAddress("alice@wonderland.lit") - )).collect(Collectors.toList()); + ).selectUserIds(secretKeys); assertEquals(Collections.singletonList(""), validEmail); - List startsWithAlice = userIds.stream().filter(SelectUserId.startsWith("Alice")).collect(Collectors.toList()); + List startsWithAlice = SelectUserId.startsWith("Alice").selectUserIds(secretKeys); assertEquals(Collections.singletonList("Alice Liddell "), startsWithAlice); - List exactMatch = userIds.stream().filter(SelectUserId.or( + List exactMatch = SelectUserId.or( SelectUserId.exactMatch(""), SelectUserId.startsWith("Not Found") - )).collect(Collectors.toList()); + ).selectUserIds(secretKeys); assertEquals(Collections.singletonList(""), exactMatch); } @Test - public void testContainsSubstring() throws PGPException { + public void testContainsSubstring() throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, 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 userIds = PGPainless.inspectKeyRing(secretKeys).getValidUserIds(); - - List containSubstring = userIds.stream().filter(SelectUserId.containsSubstring("ine")).collect(Collectors.toList()); + List containSubstring = SelectUserId.containsSubstring("ine") + .selectUserIds(secretKeys); assertEquals(Arrays.asList("wine drinker", "this is not a quine"), containSubstring); } @Test - public void testContainsEmailAddress() { + public void testContainsEmailAddress() throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().simpleEcKeyRing("Alice "); - List userIds = PGPainless.inspectKeyRing(secretKeys).getValidUserIds(); - assertEquals("Alice ", userIds.stream().filter( - SelectUserId.containsEmailAddress("alice@wonderland.lit")).findFirst().get()); - assertEquals("Alice ", userIds.stream().filter( - SelectUserId.containsEmailAddress("")).findFirst().get()); + assertEquals("Alice ", SelectUserId.containsEmailAddress("alice@wonderland.lit").firstMatch(secretKeys)); + assertEquals("Alice ", SelectUserId.containsEmailAddress("").firstMatch(secretKeys)); - assertFalse(userIds.stream().anyMatch(SelectUserId.containsEmailAddress("mad@hatter.lit"))); + assertNull(SelectUserId.containsEmailAddress("mad@hatter.lit").firstMatch(secretKeys)); } @Test - public void testAndOrNot() throws PGPException { + public void testAndOrNot() throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().simpleEcKeyRing("Alice "); secretKeys = PGPainless.modifyKeyRing(secretKeys) .addUserId("Alice ", SecretKeyRingProtector.unprotectedKeys()) @@ -87,32 +83,34 @@ public class SelectUserIdTest { .addUserId("Crazy Girl ", SecretKeyRingProtector.unprotectedKeys()) .done(); - List userIds = PGPainless.inspectKeyRing(secretKeys).getValidUserIds(); - - List or = userIds.stream().filter(SelectUserId.or( + List or = SelectUserId.or( SelectUserId.containsEmailAddress("alice@wonderland.lit"), - SelectUserId.startsWith("Alice"))).collect(Collectors.toList()); + SelectUserId.startsWith("Alice")) + .selectUserIds(secretKeys); assertEquals(Arrays.asList("Alice ", "Alice ", "Crazy Girl "), or); - List and = userIds.stream().filter(SelectUserId.and( + List and = SelectUserId.and( SelectUserId.containsEmailAddress("alice@wonderland.lit"), - SelectUserId.startsWith("Alice"))).collect(Collectors.toList()); + SelectUserId.startsWith("Alice")) + .selectUserIds(secretKeys); assertEquals(Collections.singletonList("Alice "), and); - List not = userIds.stream().filter(SelectUserId.not( - SelectUserId.startsWith("Alice"))).collect(Collectors.toList()); + List not = SelectUserId.not( + SelectUserId.startsWith("Alice")) + .selectUserIds(secretKeys); assertEquals(Arrays.asList("", "Crazy Girl "), not); } @Test - public void testFirstMatch() throws PGPException { + public void testFirstMatch() throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().simpleEcKeyRing("First UserID"); secretKeys = PGPainless.modifyKeyRing(secretKeys) .addUserId("Second UserID", SecretKeyRingProtector.unprotectedKeys()) .done(); - 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()); + assertEquals("First UserID", SelectUserId.validUserId(secretKeys).firstMatch(secretKeys)); + assertEquals("Second UserID", SelectUserId.containsSubstring("Second").firstMatch( + PGPainless.inspectKeyRing(secretKeys).getUserIds() + )); } @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 deleted file mode 100644 index 49470c75..00000000 --- a/pgpainless-core/src/test/kotlin/org/pgpainless/bouncycastle/extensions/PGPKeyRingExtensionsTest.kt +++ /dev/null @@ -1,56 +0,0 @@ -// 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 deleted file mode 100644 index d9c99c47..00000000 --- a/pgpainless-core/src/test/kotlin/org/pgpainless/bouncycastle/extensions/PGPPublicKeyExtensionsTest.kt +++ /dev/null @@ -1,60 +0,0 @@ -// 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 deleted file mode 100644 index 6dc1e4d4..00000000 --- a/pgpainless-core/src/test/kotlin/org/pgpainless/bouncycastle/extensions/PGPSecretKeyExtensionsTest.kt +++ /dev/null @@ -1,67 +0,0 @@ -// 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 deleted file mode 100644 index 90641aff..00000000 --- a/pgpainless-core/src/test/kotlin/org/pgpainless/bouncycastle/extensions/PGPSecretKeyRingExtensionsTest.kt +++ /dev/null @@ -1,59 +0,0 @@ -// 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 deleted file mode 100644 index 253c0f19..00000000 --- a/pgpainless-core/src/test/kotlin/org/pgpainless/key/KeyWithUnknownSecretKeyEncryptionMethodTest.kt +++ /dev/null @@ -1,43 +0,0 @@ -// 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 deleted file mode 100644 index e557adc3..00000000 --- a/pgpainless-core/src/test/kotlin/org/pgpainless/key/KeyWithUnsupportedSignatureSubpacketTest.kt +++ /dev/null @@ -1,38 +0,0 @@ -// 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 7f7267cd..ba87853f 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: 10](https://img.shields.io/badge/Spec%20Revision-10-blue)](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/) +[![Spec Revision: 7](https://img.shields.io/badge/Spec%20Revision-7-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.7.6" + implementation "org.pgpainless:pgpainless-sop:1.6.8" ... } @@ -34,7 +34,7 @@ dependencies { org.pgpainless pgpainless-sop - 1.7.6 + 1.6.8 ... @@ -67,7 +67,7 @@ byte[] encrypted = sop.encrypt() // Decrypt a message ByteArrayAndResult messageAndVerifications = sop.decrypt() - .verifyWithCert(cert) + .verifyWith(cert) .withKey(key) .ciphertext(encrypted) .toByteArrayAndResult(); diff --git a/pgpainless-sop/build.gradle b/pgpainless-sop/build.gradle index 53b499c6..26beec67 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 "org.pgpainless:sop-java-testfixtures:$sopJavaVersion" + testImplementation(testFixtures("org.pgpainless:sop-java:$sopJavaVersion")) implementation(project(":pgpainless-core")) api "org.pgpainless:sop-java:$sopJavaVersion" @@ -30,12 +30,6 @@ 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") diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/ArmorImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/ArmorImpl.java new file mode 100644 index 00000000..daee3a9b --- /dev/null +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/ArmorImpl.java @@ -0,0 +1,58 @@ +// 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 new file mode 100644 index 00000000..95377b12 --- /dev/null +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/ChangeKeyPasswordImpl.java @@ -0,0 +1,90 @@ +// 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 new file mode 100644 index 00000000..29483437 --- /dev/null +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DearmorImpl.java @@ -0,0 +1,42 @@ +// 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 new file mode 100644 index 00000000..d15713ca --- /dev/null +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java @@ -0,0 +1,166 @@ +// 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 new file mode 100644 index 00000000..4f74b9ee --- /dev/null +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedSignImpl.java @@ -0,0 +1,162 @@ +// 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.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); + } + } + + OutputStream sink = new NullOutputStream(); + try { + EncryptionStream signingStream = PGPainless.encryptAndOrSign() + .onOutputStream(sink) + .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 new file mode 100644 index 00000000..cdae0215 --- /dev/null +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java @@ -0,0 +1,93 @@ +// 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 new file mode 100644 index 00000000..08c58395 --- /dev/null +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java @@ -0,0 +1,188 @@ +// 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 new file mode 100644 index 00000000..be7fc9c3 --- /dev/null +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/ExtractCertImpl.java @@ -0,0 +1,60 @@ +// 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 new file mode 100644 index 00000000..b6d5fe73 --- /dev/null +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java @@ -0,0 +1,146 @@ +// 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 new file mode 100644 index 00000000..bafc2794 --- /dev/null +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineDetachImpl.java @@ -0,0 +1,152 @@ +// 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 new file mode 100644 index 00000000..dd4ab0cf --- /dev/null +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java @@ -0,0 +1,128 @@ +// 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 new file mode 100644 index 00000000..aecb891b --- /dev/null +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java @@ -0,0 +1,95 @@ +// 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 new file mode 100644 index 00000000..a2876a6e --- /dev/null +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/KeyReader.java @@ -0,0 +1,62 @@ +// 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 new file mode 100644 index 00000000..36ef4861 --- /dev/null +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/ListProfilesImpl.java @@ -0,0 +1,36 @@ +// 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 new file mode 100644 index 00000000..5badf1f1 --- /dev/null +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/MatchMakingSecretKeyRingProtector.java @@ -0,0 +1,119 @@ +// 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/NullOutputStream.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/NullOutputStream.java new file mode 100644 index 00000000..9977ba28 --- /dev/null +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/NullOutputStream.java @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop; + +import java.io.OutputStream; + +/** + * {@link OutputStream} that simply discards bytes written to it. + */ +public class NullOutputStream extends OutputStream { + @Override + public void write(int b) { + // NOP + } +} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/RevokeKeyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/RevokeKeyImpl.java new file mode 100644 index 00000000..6d72bf8f --- /dev/null +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/RevokeKeyImpl.java @@ -0,0 +1,115 @@ +// 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 new file mode 100644 index 00000000..4009d2a8 --- /dev/null +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java @@ -0,0 +1,121 @@ +// 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 new file mode 100644 index 00000000..126a5e3b --- /dev/null +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/VerificationHelper.java @@ -0,0 +1,52 @@ +// 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 new file mode 100644 index 00000000..488d95b3 --- /dev/null +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java @@ -0,0 +1,83 @@ +// 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 new file mode 100644 index 00000000..c0ce9cda --- /dev/null +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/package-info.java @@ -0,0 +1,8 @@ +// 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 deleted file mode 100644 index 40ac811d..00000000 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ArmorImpl.kt +++ /dev/null @@ -1,48 +0,0 @@ -// 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 deleted file mode 100644 index a9aaf1e4..00000000 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ChangeKeyPasswordImpl.kt +++ /dev/null @@ -1,81 +0,0 @@ -// 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 deleted file mode 100644 index 9d196004..00000000 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DearmorImpl.kt +++ /dev/null @@ -1,38 +0,0 @@ -// 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 deleted file mode 100644 index de2b2b3c..00000000 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DecryptImpl.kt +++ /dev/null @@ -1,125 +0,0 @@ -// 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 deleted file mode 100644 index 19bc782b..00000000 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DetachedSignImpl.kt +++ /dev/null @@ -1,129 +0,0 @@ -// 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 deleted file mode 100644 index 08472144..00000000 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DetachedVerifyImpl.kt +++ /dev/null @@ -1,71 +0,0 @@ -// 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 deleted file mode 100644 index b227561e..00000000 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/EncryptImpl.kt +++ /dev/null @@ -1,155 +0,0 @@ -// 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 deleted file mode 100644 index 7fe66ee5..00000000 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ExtractCertImpl.kt +++ /dev/null @@ -1,47 +0,0 @@ -// 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 deleted file mode 100644 index f8297c56..00000000 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/GenerateKeyImpl.kt +++ /dev/null @@ -1,137 +0,0 @@ -// 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 deleted file mode 100644 index 88ca8c54..00000000 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineDetachImpl.kt +++ /dev/null @@ -1,134 +0,0 @@ -// 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 deleted file mode 100644 index bd77b553..00000000 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineSignImpl.kt +++ /dev/null @@ -1,126 +0,0 @@ -// 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 deleted file mode 100644 index 0b4e7d2f..00000000 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineVerifyImpl.kt +++ /dev/null @@ -1,73 +0,0 @@ -// 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 deleted file mode 100644 index 2ce608ca..00000000 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/KeyReader.kt +++ /dev/null @@ -1,77 +0,0 @@ -// 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 deleted file mode 100644 index 39a5151d..00000000 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ListProfilesImpl.kt +++ /dev/null @@ -1,20 +0,0 @@ -// 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 deleted file mode 100644 index 13347721..00000000 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/MatchMakingSecretKeyRingProtector.kt +++ /dev/null @@ -1,88 +0,0 @@ -// 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 deleted file mode 100644 index ecc87e62..00000000 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/RevokeKeyImpl.kt +++ /dev/null @@ -1,95 +0,0 @@ -// 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 deleted file mode 100644 index 16f54a22..00000000 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/SOPImpl.kt +++ /dev/null @@ -1,56 +0,0 @@ -// 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 deleted file mode 100644 index 43b4c64f..00000000 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/SOPVImpl.kt +++ /dev/null @@ -1,24 +0,0 @@ -// 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 deleted file mode 100644 index 9198e3b7..00000000 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/VerificationHelper.kt +++ /dev/null @@ -1,48 +0,0 @@ -// 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 deleted file mode 100644 index 94b9c016..00000000 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/VersionImpl.kt +++ /dev/null @@ -1,76 +0,0 @@ -// 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 deleted file mode 100644 index d0f47796..00000000 --- a/pgpainless-sop/src/main/resources/pgpainless-sop.properties +++ /dev/null @@ -1,4 +0,0 @@ -# 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 82688bbf..3830912a 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/ArmorTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/ArmorTest.java @@ -4,19 +4,30 @@ package org.pgpainless.sop; -import org.junit.jupiter.api.Test; -import org.pgpainless.PGPainless; -import org.pgpainless.util.ArmorUtils; +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 static org.junit.jupiter.api.Assertions.assertArrayEquals; +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; public class ArmorTest { @Test - public void armor() throws IOException { + public void labelIsNotSupported() { + assertThrows(SOPGPException.UnsupportedOption.class, () -> new SOPImpl().armor().label(ArmorLabel.Sig)); + } + + @Test + public void armor() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { byte[] data = PGPainless.generateKeyRing().modernKeyRing("Alice").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 83778106..58dfaa62 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/CarolKeySignEncryptRoundtripTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/CarolKeySignEncryptRoundtripTest.java @@ -11,8 +11,7 @@ import java.io.IOException; import org.junit.jupiter.api.Test; import sop.ByteArrayAndResult; import sop.DecryptionResult; -import sop.EncryptionResult; -import sop.ReadyWithResult; +import sop.Ready; import sop.testsuite.assertions.VerificationListAssert; public class CarolKeySignEncryptRoundtripTest { @@ -277,11 +276,11 @@ public class CarolKeySignEncryptRoundtripTest { public void regressionTest() throws IOException { SOPImpl sop = new SOPImpl(); byte[] msg = "Hello, World!\n".getBytes(); - ReadyWithResult encryption = sop.encrypt() + Ready encryption = sop.encrypt() .signWith(CAROL_KEY.getBytes()) .withCert(BOB_CERT.getBytes()) .plaintext(msg); - byte[] ciphertext = encryption.toByteArrayAndResult().getBytes(); + byte[] ciphertext = encryption.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 7b2c9131..9238697a 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 aa366ef1..ccfeaf41 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/EncryptDecryptRoundTripTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/EncryptDecryptRoundTripTest.java @@ -64,7 +64,6 @@ public class EncryptDecryptRoundTripTest { .withCert(aliceCert) .withCert(bobCert) .plaintext(message) - .toByteArrayAndResult() .getBytes(); ByteArrayAndResult bytesAndResult = sop.decrypt() @@ -98,7 +97,6 @@ public class EncryptDecryptRoundTripTest { .withCert(aliceCertNoArmor) .noArmor() .plaintext(message) - .toByteArrayAndResult() .getBytes(); ByteArrayAndResult bytesAndResult = sop.decrypt() @@ -120,7 +118,6 @@ public class EncryptDecryptRoundTripTest { byte[] encrypted = sop.encrypt() .withPassword("passphr4s3") .plaintext(message) - .toByteArrayAndResult() .getBytes(); ByteArrayAndResult bytesAndResult = sop.decrypt() @@ -144,7 +141,6 @@ public class EncryptDecryptRoundTripTest { sop.encrypt() .withPassword("passphr4s3") .plaintext(message) - .toByteArrayAndResult() .getInputStream() ) .toByteArrayAndResult(); @@ -162,7 +158,6 @@ public class EncryptDecryptRoundTripTest { byte[] encrypted = sop.encrypt() .withPassword("passphr4s3 ") .plaintext(message) - .toByteArrayAndResult() .getBytes(); ByteArrayAndResult bytesAndResult = sop.decrypt() @@ -183,7 +178,6 @@ public class EncryptDecryptRoundTripTest { byte[] encrypted = sop.encrypt() .withCert(bobCert) .plaintext(message) - .toByteArrayAndResult() .getBytes(); DecryptionResult result = sop.decrypt() @@ -202,7 +196,6 @@ public class EncryptDecryptRoundTripTest { byte[] encrypted = sop.encrypt() .withCert(bobCert) .plaintext(message) - .toByteArrayAndResult() .getBytes(); assertThrows(SOPGPException.MissingArg.class, () -> sop @@ -273,7 +266,6 @@ public class EncryptDecryptRoundTripTest { byte[] ciphertext = sop.encrypt() .withCert(cert) .plaintext(plaintext) - .toByteArrayAndResult() .getBytes(); byte[] decrypted = sop.decrypt() @@ -316,7 +308,6 @@ public class EncryptDecryptRoundTripTest { .withCert(cert1) .withCert(cert2) .plaintext(plaintext) - .toByteArrayAndResult() .getBytes(); byte[] decrypted = sop.decrypt() @@ -348,7 +339,6 @@ public class EncryptDecryptRoundTripTest { byte[] ciphertext = sop.encrypt() .withCert(cert) .plaintext(plaintext) - .toByteArrayAndResult() .getBytes(); assertThrows(SOPGPException.KeyIsProtected.class, @@ -581,7 +571,6 @@ 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 521cdfe0..5894bfa7 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/GenerateKeyTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/GenerateKeyTest.java @@ -102,12 +102,8 @@ public class GenerateKeyTest { } @Test - public void generateKeyWithNewlinesInUserId() throws IOException { - byte[] keyBytes = sop.generateKey() - .userId("Foo\n\nBar") - .generate() - .getBytes(); - - assertTrue(new String(keyBytes).contains("Foo\\n\\nBar")); + public void nullProfile() { + assertThrows(SOPGPException.UnsupportedProfile.class, () -> + sop.generateKey().profile((String) null)); } } 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 efcd51c4..5139bbf6 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_legacy.EdDSALegacyCurve; +import org.pgpainless.key.generation.type.eddsa.EdDSACurve; 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_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER)) + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER)) .addUserId("Non Signing ") .build(); nonSigningKey = ArmorUtils.toAsciiArmoredString(key).getBytes(StandardCharsets.UTF_8); nonSigningCert = sop.extractCert().key(nonSigningKey).getBytes(); key = PGPainless.buildKeyRing() - .addSubkey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.SIGN_DATA)) - .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER)) + .addSubkey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.SIGN_DATA)) + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._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 e15a04cc..9158f11e 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/ListProfilesTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/ListProfilesTest.java @@ -37,4 +37,9 @@ 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 deleted file mode 100644 index 23fd9840..00000000 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/VerifyLegacySignatureTest.java +++ /dev/null @@ -1,164 +0,0 @@ -// 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 825b5e86..32d2c2e0 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/VersionTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/VersionTest.java @@ -72,11 +72,4 @@ 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 deleted file mode 100644 index a1994b9d..00000000 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/package-info.java +++ /dev/null @@ -1,9 +0,0 @@ -// 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 baf595d3..78ba6737 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_legacy.EdDSALegacyCurve; -import org.pgpainless.key.generation.type.xdh_legacy.XDHLegacySpec; +import org.pgpainless.key.generation.type.eddsa.EdDSACurve; +import org.pgpainless.key.generation.type.xdh.XDHSpec; 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_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)) + .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)) .build(); Iterator keys = secretKeys.getPublicKeys(); long primaryKeyId = keys.next().getKeyID(); @@ -75,7 +75,6 @@ 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 deleted file mode 100644 index 5da4307d..00000000 --- a/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/package-info.java +++ /dev/null @@ -1,9 +0,0 @@ -// 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 new file mode 100644 index 00000000..d67967b9 --- /dev/null +++ b/sop-java-picocli/README.md @@ -0,0 +1,8 @@ + +# 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 new file mode 100644 index 00000000..dd59c615 --- /dev/null +++ b/sop-java/README.md @@ -0,0 +1,8 @@ + +# 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 bc2515a4..420e3067 100644 --- a/version.gradle +++ b/version.gradle @@ -4,15 +4,16 @@ allprojects { ext { - shortVersion = '1.7.7' - isSnapshot = true - javaSourceCompatibility = 11 - bouncyCastleVersion = '1.81' + shortVersion = '1.6.8' + isSnapshot = false + pgpainlessMinAndroidSdk = 10 + javaSourceCompatibility = 1.8 + bouncyCastleVersion = '1.77' bouncyPgVersion = bouncyCastleVersion junitVersion = '5.8.2' - logbackVersion = '1.5.13' + logbackVersion = '1.2.13' mockitoVersion = '4.5.1' slf4jVersion = '1.7.36' - sopJavaVersion = '10.1.1' + sopJavaVersion = '7.0.2' } }

- * 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 deleted file mode 100644 index 6a3a6214..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/AEADAlgorithm.kt +++ /dev/null @@ -1,41 +0,0 @@ -// 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 deleted file mode 100644 index 867bf1b8..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/AlgorithmSuite.kt +++ /dev/null @@ -1,47 +0,0 @@ -// 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 deleted file mode 100644 index 5617109c..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/CertificationType.kt +++ /dev/null @@ -1,33 +0,0 @@ -// 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 deleted file mode 100644 index da4085bd..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/CompressionAlgorithm.kt +++ /dev/null @@ -1,48 +0,0 @@ -// 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 deleted file mode 100644 index e41a4605..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/DocumentSignatureType.kt +++ /dev/null @@ -1,16 +0,0 @@ -// 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 deleted file mode 100644 index 1b4bbe6e..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/EncryptionPurpose.kt +++ /dev/null @@ -1,14 +0,0 @@ -// 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 deleted file mode 100644 index ec1cfe21..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/Feature.kt +++ /dev/null @@ -1,90 +0,0 @@ -// 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 deleted file mode 100644 index 3360e7fe..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/HashAlgorithm.kt +++ /dev/null @@ -1,70 +0,0 @@ -// 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 deleted file mode 100644 index c5c4e103..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/KeyFlag.kt +++ /dev/null @@ -1,73 +0,0 @@ -// 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 deleted file mode 100644 index 17ec90e4..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/OpenPgpPacket.kt +++ /dev/null @@ -1,45 +0,0 @@ -// 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 deleted file mode 100644 index b8fc6836..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/PublicKeyAlgorithm.kt +++ /dev/null @@ -1,94 +0,0 @@ -// 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 deleted file mode 100644 index ba288469..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/RevocationState.kt +++ /dev/null @@ -1,83 +0,0 @@ -// 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 deleted file mode 100644 index 1f95ad7b..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/RevocationStateType.kt +++ /dev/null @@ -1,16 +0,0 @@ -// 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 deleted file mode 100644 index a0efd618..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/SignatureSubpacket.kt +++ /dev/null @@ -1,361 +0,0 @@ -// 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 deleted file mode 100644 index be6917df..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/SignatureType.kt +++ /dev/null @@ -1,199 +0,0 @@ -// 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 deleted file mode 100644 index 74b2a56b..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/StreamEncoding.kt +++ /dev/null @@ -1,62 +0,0 @@ -// 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 deleted file mode 100644 index bfd32343..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/SymmetricKeyAlgorithm.kt +++ /dev/null @@ -1,89 +0,0 @@ -// 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 deleted file mode 100644 index 5b9f4c40..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/Trustworthiness.kt +++ /dev/null @@ -1,138 +0,0 @@ -// 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 - *