mirror of
https://github.com/pgpainless/pgpainless.git
synced 2024-11-19 02:42:05 +01:00
Merge pull request #404 from pgpainless/pgpainless2
PGPainless 2.0 - Kotlin Conversion
This commit is contained in:
commit
6fdf136024
373 changed files with 20253 additions and 26986 deletions
591
.editorconfig
591
.editorconfig
|
@ -1,513 +1,51 @@
|
|||
# SPDX-FileCopyrightText: 2021 Paul Schaub <info@pgpainless.org>
|
||||
# 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 <project root>/.editorconfig
|
||||
# and making sure your editor is set to read settings from .editorconfig files.
|
||||
#
|
||||
# SPDX-License-Identifier: CC0-1.0
|
||||
# It includes editor-specific config options for IntelliJ IDEA.
|
||||
#
|
||||
# If any option is wrong, PR are welcome
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 4
|
||||
[{*.kt,*.kts}]
|
||||
indent_style = space
|
||||
insert_final_newline = false
|
||||
max_line_length = 120
|
||||
tab_width = 4
|
||||
ij_continuation_indent_size = 8
|
||||
ij_formatter_off_tag = @formatter:off
|
||||
ij_formatter_on_tag = @formatter:on
|
||||
ij_formatter_tags_enabled = false
|
||||
ij_smart_tabs = false
|
||||
ij_visual_guides = none
|
||||
ij_wrap_on_typing = false
|
||||
|
||||
[*.java]
|
||||
ij_java_align_consecutive_assignments = false
|
||||
ij_java_align_consecutive_variable_declarations = false
|
||||
ij_java_align_group_field_declarations = false
|
||||
ij_java_align_multiline_annotation_parameters = false
|
||||
ij_java_align_multiline_array_initializer_expression = false
|
||||
ij_java_align_multiline_assignment = false
|
||||
ij_java_align_multiline_binary_operation = false
|
||||
ij_java_align_multiline_chained_methods = false
|
||||
ij_java_align_multiline_extends_list = false
|
||||
ij_java_align_multiline_for = true
|
||||
ij_java_align_multiline_method_parentheses = false
|
||||
ij_java_align_multiline_parameters = true
|
||||
ij_java_align_multiline_parameters_in_calls = false
|
||||
ij_java_align_multiline_parenthesized_expression = false
|
||||
ij_java_align_multiline_records = true
|
||||
ij_java_align_multiline_resources = true
|
||||
ij_java_align_multiline_ternary_operation = false
|
||||
ij_java_align_multiline_text_blocks = false
|
||||
ij_java_align_multiline_throws_list = false
|
||||
ij_java_align_subsequent_simple_methods = false
|
||||
ij_java_align_throws_keyword = false
|
||||
ij_java_annotation_parameter_wrap = off
|
||||
ij_java_array_initializer_new_line_after_left_brace = false
|
||||
ij_java_array_initializer_right_brace_on_new_line = false
|
||||
ij_java_array_initializer_wrap = off
|
||||
ij_java_assert_statement_colon_on_next_line = false
|
||||
ij_java_assert_statement_wrap = off
|
||||
ij_java_assignment_wrap = off
|
||||
ij_java_binary_operation_sign_on_next_line = false
|
||||
ij_java_binary_operation_wrap = off
|
||||
ij_java_blank_lines_after_anonymous_class_header = 0
|
||||
ij_java_blank_lines_after_class_header = 0
|
||||
ij_java_blank_lines_after_imports = 1
|
||||
ij_java_blank_lines_after_package = 1
|
||||
ij_java_blank_lines_around_class = 1
|
||||
ij_java_blank_lines_around_field = 0
|
||||
ij_java_blank_lines_around_field_in_interface = 0
|
||||
ij_java_blank_lines_around_initializer = 1
|
||||
ij_java_blank_lines_around_method = 1
|
||||
ij_java_blank_lines_around_method_in_interface = 1
|
||||
ij_java_blank_lines_before_class_end = 0
|
||||
ij_java_blank_lines_before_imports = 1
|
||||
ij_java_blank_lines_before_method_body = 0
|
||||
ij_java_blank_lines_before_package = 0
|
||||
ij_java_block_brace_style = end_of_line
|
||||
ij_java_block_comment_at_first_column = true
|
||||
ij_java_builder_methods = none
|
||||
ij_java_call_parameters_new_line_after_left_paren = false
|
||||
ij_java_call_parameters_right_paren_on_new_line = false
|
||||
ij_java_call_parameters_wrap = off
|
||||
ij_java_case_statement_on_separate_line = true
|
||||
ij_java_catch_on_new_line = false
|
||||
ij_java_class_annotation_wrap = split_into_lines
|
||||
ij_java_class_brace_style = end_of_line
|
||||
ij_java_class_count_to_use_import_on_demand = 10000
|
||||
ij_java_class_names_in_javadoc = 1
|
||||
ij_java_do_not_indent_top_level_class_members = false
|
||||
ij_java_do_not_wrap_after_single_annotation = false
|
||||
ij_java_do_while_brace_force = never
|
||||
ij_java_doc_add_blank_line_after_description = true
|
||||
ij_java_doc_add_blank_line_after_param_comments = false
|
||||
ij_java_doc_add_blank_line_after_return = false
|
||||
ij_java_doc_add_p_tag_on_empty_lines = true
|
||||
ij_java_doc_align_exception_comments = true
|
||||
ij_java_doc_align_param_comments = true
|
||||
ij_java_doc_do_not_wrap_if_one_line = false
|
||||
ij_java_doc_enable_formatting = true
|
||||
ij_java_doc_enable_leading_asterisks = true
|
||||
ij_java_doc_indent_on_continuation = false
|
||||
ij_java_doc_keep_empty_lines = true
|
||||
ij_java_doc_keep_empty_parameter_tag = true
|
||||
ij_java_doc_keep_empty_return_tag = true
|
||||
ij_java_doc_keep_empty_throws_tag = true
|
||||
ij_java_doc_keep_invalid_tags = true
|
||||
ij_java_doc_param_description_on_new_line = false
|
||||
ij_java_doc_preserve_line_breaks = false
|
||||
ij_java_doc_use_throws_not_exception_tag = true
|
||||
ij_java_else_on_new_line = false
|
||||
ij_java_enum_constants_wrap = off
|
||||
ij_java_extends_keyword_wrap = off
|
||||
ij_java_extends_list_wrap = off
|
||||
ij_java_field_annotation_wrap = split_into_lines
|
||||
ij_java_finally_on_new_line = false
|
||||
ij_java_for_brace_force = never
|
||||
ij_java_for_statement_new_line_after_left_paren = false
|
||||
ij_java_for_statement_right_paren_on_new_line = false
|
||||
ij_java_for_statement_wrap = off
|
||||
ij_java_generate_final_locals = false
|
||||
ij_java_generate_final_parameters = false
|
||||
ij_java_if_brace_force = never
|
||||
ij_java_imports_layout = $*,|,java.**,javax.**,|,*
|
||||
ij_java_indent_case_from_switch = true
|
||||
ij_java_insert_inner_class_imports = false
|
||||
ij_java_insert_override_annotation = true
|
||||
ij_java_keep_blank_lines_before_right_brace = 2
|
||||
ij_java_keep_blank_lines_between_package_declaration_and_header = 2
|
||||
ij_java_keep_blank_lines_in_code = 2
|
||||
ij_java_keep_blank_lines_in_declarations = 2
|
||||
ij_java_keep_builder_methods_indents = false
|
||||
ij_java_keep_control_statement_in_one_line = true
|
||||
ij_java_keep_first_column_comment = true
|
||||
ij_java_keep_indents_on_empty_lines = false
|
||||
ij_java_keep_line_breaks = true
|
||||
ij_java_keep_multiple_expressions_in_one_line = false
|
||||
ij_java_keep_simple_blocks_in_one_line = false
|
||||
ij_java_keep_simple_classes_in_one_line = false
|
||||
ij_java_keep_simple_lambdas_in_one_line = false
|
||||
ij_java_keep_simple_methods_in_one_line = false
|
||||
ij_java_label_indent_absolute = false
|
||||
ij_java_label_indent_size = 0
|
||||
ij_java_lambda_brace_style = end_of_line
|
||||
ij_java_layout_static_imports_separately = true
|
||||
ij_java_line_comment_add_space = false
|
||||
ij_java_line_comment_at_first_column = true
|
||||
ij_java_method_annotation_wrap = split_into_lines
|
||||
ij_java_method_brace_style = end_of_line
|
||||
ij_java_method_call_chain_wrap = off
|
||||
ij_java_method_parameters_new_line_after_left_paren = false
|
||||
ij_java_method_parameters_right_paren_on_new_line = false
|
||||
ij_java_method_parameters_wrap = off
|
||||
ij_java_modifier_list_wrap = false
|
||||
ij_java_names_count_to_use_import_on_demand = 1000
|
||||
ij_java_new_line_after_lparen_in_record_header = false
|
||||
ij_java_packages_to_use_import_on_demand = java.awt.*,javax.swing.*
|
||||
ij_java_parameter_annotation_wrap = off
|
||||
ij_java_parentheses_expression_new_line_after_left_paren = false
|
||||
ij_java_parentheses_expression_right_paren_on_new_line = false
|
||||
ij_java_place_assignment_sign_on_next_line = false
|
||||
ij_java_prefer_longer_names = true
|
||||
ij_java_prefer_parameters_wrap = false
|
||||
ij_java_record_components_wrap = normal
|
||||
ij_java_repeat_synchronized = true
|
||||
ij_java_replace_instanceof_and_cast = false
|
||||
ij_java_replace_null_check = true
|
||||
ij_java_replace_sum_lambda_with_method_ref = true
|
||||
ij_java_resource_list_new_line_after_left_paren = false
|
||||
ij_java_resource_list_right_paren_on_new_line = false
|
||||
ij_java_resource_list_wrap = off
|
||||
ij_java_rparen_on_new_line_in_record_header = false
|
||||
ij_java_space_after_closing_angle_bracket_in_type_argument = false
|
||||
ij_java_space_after_colon = true
|
||||
ij_java_space_after_comma = true
|
||||
ij_java_space_after_comma_in_type_arguments = true
|
||||
ij_java_space_after_for_semicolon = true
|
||||
ij_java_space_after_quest = true
|
||||
ij_java_space_after_type_cast = true
|
||||
ij_java_space_before_annotation_array_initializer_left_brace = false
|
||||
ij_java_space_before_annotation_parameter_list = false
|
||||
ij_java_space_before_array_initializer_left_brace = false
|
||||
ij_java_space_before_catch_keyword = true
|
||||
ij_java_space_before_catch_left_brace = true
|
||||
ij_java_space_before_catch_parentheses = true
|
||||
ij_java_space_before_class_left_brace = true
|
||||
ij_java_space_before_colon = true
|
||||
ij_java_space_before_colon_in_foreach = true
|
||||
ij_java_space_before_comma = false
|
||||
ij_java_space_before_do_left_brace = true
|
||||
ij_java_space_before_else_keyword = true
|
||||
ij_java_space_before_else_left_brace = true
|
||||
ij_java_space_before_finally_keyword = true
|
||||
ij_java_space_before_finally_left_brace = true
|
||||
ij_java_space_before_for_left_brace = true
|
||||
ij_java_space_before_for_parentheses = true
|
||||
ij_java_space_before_for_semicolon = false
|
||||
ij_java_space_before_if_left_brace = true
|
||||
ij_java_space_before_if_parentheses = true
|
||||
ij_java_space_before_method_call_parentheses = false
|
||||
ij_java_space_before_method_left_brace = true
|
||||
ij_java_space_before_method_parentheses = false
|
||||
ij_java_space_before_opening_angle_bracket_in_type_parameter = false
|
||||
ij_java_space_before_quest = true
|
||||
ij_java_space_before_switch_left_brace = true
|
||||
ij_java_space_before_switch_parentheses = true
|
||||
ij_java_space_before_synchronized_left_brace = true
|
||||
ij_java_space_before_synchronized_parentheses = true
|
||||
ij_java_space_before_try_left_brace = true
|
||||
ij_java_space_before_try_parentheses = true
|
||||
ij_java_space_before_type_parameter_list = false
|
||||
ij_java_space_before_while_keyword = true
|
||||
ij_java_space_before_while_left_brace = true
|
||||
ij_java_space_before_while_parentheses = true
|
||||
ij_java_space_inside_one_line_enum_braces = false
|
||||
ij_java_space_within_empty_array_initializer_braces = false
|
||||
ij_java_space_within_empty_method_call_parentheses = false
|
||||
ij_java_space_within_empty_method_parentheses = false
|
||||
ij_java_spaces_around_additive_operators = true
|
||||
ij_java_spaces_around_assignment_operators = true
|
||||
ij_java_spaces_around_bitwise_operators = true
|
||||
ij_java_spaces_around_equality_operators = true
|
||||
ij_java_spaces_around_lambda_arrow = true
|
||||
ij_java_spaces_around_logical_operators = true
|
||||
ij_java_spaces_around_method_ref_dbl_colon = false
|
||||
ij_java_spaces_around_multiplicative_operators = true
|
||||
ij_java_spaces_around_relational_operators = true
|
||||
ij_java_spaces_around_shift_operators = true
|
||||
ij_java_spaces_around_type_bounds_in_type_parameters = true
|
||||
ij_java_spaces_around_unary_operator = false
|
||||
ij_java_spaces_within_angle_brackets = false
|
||||
ij_java_spaces_within_annotation_parentheses = false
|
||||
ij_java_spaces_within_array_initializer_braces = false
|
||||
ij_java_spaces_within_braces = false
|
||||
ij_java_spaces_within_brackets = false
|
||||
ij_java_spaces_within_cast_parentheses = false
|
||||
ij_java_spaces_within_catch_parentheses = false
|
||||
ij_java_spaces_within_for_parentheses = false
|
||||
ij_java_spaces_within_if_parentheses = false
|
||||
ij_java_spaces_within_method_call_parentheses = false
|
||||
ij_java_spaces_within_method_parentheses = false
|
||||
ij_java_spaces_within_parentheses = false
|
||||
ij_java_spaces_within_record_header = false
|
||||
ij_java_spaces_within_switch_parentheses = false
|
||||
ij_java_spaces_within_synchronized_parentheses = false
|
||||
ij_java_spaces_within_try_parentheses = false
|
||||
ij_java_spaces_within_while_parentheses = false
|
||||
ij_java_special_else_if_treatment = true
|
||||
ij_java_subclass_name_suffix = Impl
|
||||
ij_java_ternary_operation_signs_on_next_line = false
|
||||
ij_java_ternary_operation_wrap = off
|
||||
ij_java_test_name_suffix = Test
|
||||
ij_java_throws_keyword_wrap = off
|
||||
ij_java_throws_list_wrap = off
|
||||
ij_java_use_external_annotations = false
|
||||
ij_java_use_fq_class_names = false
|
||||
ij_java_use_relative_indents = false
|
||||
ij_java_use_single_class_imports = true
|
||||
ij_java_variable_annotation_wrap = off
|
||||
ij_java_visibility = public
|
||||
ij_java_while_brace_force = never
|
||||
ij_java_while_on_new_line = false
|
||||
ij_java_wrap_comments = false
|
||||
ij_java_wrap_first_method_in_call_chain = false
|
||||
ij_java_wrap_long_lines = false
|
||||
|
||||
[*.properties]
|
||||
ij_properties_align_group_field_declarations = false
|
||||
ij_properties_keep_blank_lines = false
|
||||
ij_properties_key_value_delimiter = equals
|
||||
ij_properties_spaces_around_key_value_delimiter = false
|
||||
|
||||
[.editorconfig]
|
||||
ij_editorconfig_align_group_field_declarations = false
|
||||
ij_editorconfig_space_after_colon = false
|
||||
ij_editorconfig_space_after_comma = true
|
||||
ij_editorconfig_space_before_colon = false
|
||||
ij_editorconfig_space_before_comma = false
|
||||
ij_editorconfig_spaces_around_assignment_operators = true
|
||||
|
||||
[{*.ant,*.fxml,*.jhm,*.jnlp,*.jrxml,*.jspx,*.pom,*.rng,*.tagx,*.tld,*.wsdl,*.xml,*.xsd,*.xsl,*.xslt,*.xul}]
|
||||
ij_xml_align_attributes = true
|
||||
ij_xml_align_text = false
|
||||
ij_xml_attribute_wrap = normal
|
||||
ij_xml_block_comment_at_first_column = true
|
||||
ij_xml_keep_blank_lines = 2
|
||||
ij_xml_keep_indents_on_empty_lines = false
|
||||
ij_xml_keep_line_breaks = true
|
||||
ij_xml_keep_line_breaks_in_text = true
|
||||
ij_xml_keep_whitespaces = false
|
||||
ij_xml_keep_whitespaces_around_cdata = preserve
|
||||
ij_xml_keep_whitespaces_inside_cdata = false
|
||||
ij_xml_line_comment_at_first_column = true
|
||||
ij_xml_space_after_tag_name = false
|
||||
ij_xml_space_around_equals_in_attribute = false
|
||||
ij_xml_space_inside_empty_tag = false
|
||||
ij_xml_text_wrap = normal
|
||||
ij_xml_use_custom_settings = false
|
||||
|
||||
[{*.bash,*.sh,*.zsh}]
|
||||
indent_size = 2
|
||||
tab_width = 2
|
||||
ij_shell_binary_ops_start_line = false
|
||||
ij_shell_keep_column_alignment_padding = false
|
||||
ij_shell_minify_program = false
|
||||
ij_shell_redirect_followed_by_space = false
|
||||
ij_shell_switch_cases_indented = false
|
||||
ij_shell_use_unix_line_separator = true
|
||||
|
||||
[{*.gant,*.gradle,*.groovy,*.gy}]
|
||||
ij_groovy_align_group_field_declarations = false
|
||||
ij_groovy_align_multiline_array_initializer_expression = false
|
||||
ij_groovy_align_multiline_assignment = false
|
||||
ij_groovy_align_multiline_binary_operation = false
|
||||
ij_groovy_align_multiline_chained_methods = false
|
||||
ij_groovy_align_multiline_extends_list = false
|
||||
ij_groovy_align_multiline_for = true
|
||||
ij_groovy_align_multiline_list_or_map = true
|
||||
ij_groovy_align_multiline_method_parentheses = false
|
||||
ij_groovy_align_multiline_parameters = true
|
||||
ij_groovy_align_multiline_parameters_in_calls = false
|
||||
ij_groovy_align_multiline_resources = true
|
||||
ij_groovy_align_multiline_ternary_operation = false
|
||||
ij_groovy_align_multiline_throws_list = false
|
||||
ij_groovy_align_named_args_in_map = true
|
||||
ij_groovy_align_throws_keyword = false
|
||||
ij_groovy_array_initializer_new_line_after_left_brace = false
|
||||
ij_groovy_array_initializer_right_brace_on_new_line = false
|
||||
ij_groovy_array_initializer_wrap = off
|
||||
ij_groovy_assert_statement_wrap = off
|
||||
ij_groovy_assignment_wrap = off
|
||||
ij_groovy_binary_operation_wrap = off
|
||||
ij_groovy_blank_lines_after_class_header = 0
|
||||
ij_groovy_blank_lines_after_imports = 1
|
||||
ij_groovy_blank_lines_after_package = 1
|
||||
ij_groovy_blank_lines_around_class = 1
|
||||
ij_groovy_blank_lines_around_field = 0
|
||||
ij_groovy_blank_lines_around_field_in_interface = 0
|
||||
ij_groovy_blank_lines_around_method = 1
|
||||
ij_groovy_blank_lines_around_method_in_interface = 1
|
||||
ij_groovy_blank_lines_before_imports = 1
|
||||
ij_groovy_blank_lines_before_method_body = 0
|
||||
ij_groovy_blank_lines_before_package = 0
|
||||
ij_groovy_block_brace_style = end_of_line
|
||||
ij_groovy_block_comment_at_first_column = true
|
||||
ij_groovy_call_parameters_new_line_after_left_paren = false
|
||||
ij_groovy_call_parameters_right_paren_on_new_line = false
|
||||
ij_groovy_call_parameters_wrap = off
|
||||
ij_groovy_catch_on_new_line = false
|
||||
ij_groovy_class_annotation_wrap = split_into_lines
|
||||
ij_groovy_class_brace_style = end_of_line
|
||||
ij_groovy_class_count_to_use_import_on_demand = 5
|
||||
ij_groovy_do_while_brace_force = never
|
||||
ij_groovy_else_on_new_line = false
|
||||
ij_groovy_enum_constants_wrap = off
|
||||
ij_groovy_extends_keyword_wrap = off
|
||||
ij_groovy_extends_list_wrap = off
|
||||
ij_groovy_field_annotation_wrap = split_into_lines
|
||||
ij_groovy_finally_on_new_line = false
|
||||
ij_groovy_for_brace_force = never
|
||||
ij_groovy_for_statement_new_line_after_left_paren = false
|
||||
ij_groovy_for_statement_right_paren_on_new_line = false
|
||||
ij_groovy_for_statement_wrap = off
|
||||
ij_groovy_if_brace_force = never
|
||||
ij_groovy_import_annotation_wrap = 2
|
||||
ij_groovy_imports_layout = *,|,javax.**,java.**,|,$*
|
||||
ij_groovy_indent_case_from_switch = true
|
||||
ij_groovy_indent_label_blocks = true
|
||||
ij_groovy_insert_inner_class_imports = false
|
||||
ij_groovy_keep_blank_lines_before_right_brace = 2
|
||||
ij_groovy_keep_blank_lines_in_code = 2
|
||||
ij_groovy_keep_blank_lines_in_declarations = 2
|
||||
ij_groovy_keep_control_statement_in_one_line = true
|
||||
ij_groovy_keep_first_column_comment = true
|
||||
ij_groovy_keep_indents_on_empty_lines = false
|
||||
ij_groovy_keep_line_breaks = true
|
||||
ij_groovy_keep_multiple_expressions_in_one_line = false
|
||||
ij_groovy_keep_simple_blocks_in_one_line = false
|
||||
ij_groovy_keep_simple_classes_in_one_line = true
|
||||
ij_groovy_keep_simple_lambdas_in_one_line = true
|
||||
ij_groovy_keep_simple_methods_in_one_line = true
|
||||
ij_groovy_label_indent_absolute = false
|
||||
ij_groovy_label_indent_size = 0
|
||||
ij_groovy_lambda_brace_style = end_of_line
|
||||
ij_groovy_layout_static_imports_separately = true
|
||||
ij_groovy_line_comment_add_space = false
|
||||
ij_groovy_line_comment_at_first_column = true
|
||||
ij_groovy_method_annotation_wrap = split_into_lines
|
||||
ij_groovy_method_brace_style = end_of_line
|
||||
ij_groovy_method_call_chain_wrap = off
|
||||
ij_groovy_method_parameters_new_line_after_left_paren = false
|
||||
ij_groovy_method_parameters_right_paren_on_new_line = false
|
||||
ij_groovy_method_parameters_wrap = off
|
||||
ij_groovy_modifier_list_wrap = false
|
||||
ij_groovy_names_count_to_use_import_on_demand = 3
|
||||
ij_groovy_parameter_annotation_wrap = off
|
||||
ij_groovy_parentheses_expression_new_line_after_left_paren = false
|
||||
ij_groovy_parentheses_expression_right_paren_on_new_line = false
|
||||
ij_groovy_prefer_parameters_wrap = false
|
||||
ij_groovy_resource_list_new_line_after_left_paren = false
|
||||
ij_groovy_resource_list_right_paren_on_new_line = false
|
||||
ij_groovy_resource_list_wrap = off
|
||||
ij_groovy_space_after_assert_separator = true
|
||||
ij_groovy_space_after_colon = true
|
||||
ij_groovy_space_after_comma = true
|
||||
ij_groovy_space_after_comma_in_type_arguments = true
|
||||
ij_groovy_space_after_for_semicolon = true
|
||||
ij_groovy_space_after_quest = true
|
||||
ij_groovy_space_after_type_cast = true
|
||||
ij_groovy_space_before_annotation_parameter_list = false
|
||||
ij_groovy_space_before_array_initializer_left_brace = false
|
||||
ij_groovy_space_before_assert_separator = false
|
||||
ij_groovy_space_before_catch_keyword = true
|
||||
ij_groovy_space_before_catch_left_brace = true
|
||||
ij_groovy_space_before_catch_parentheses = true
|
||||
ij_groovy_space_before_class_left_brace = true
|
||||
ij_groovy_space_before_closure_left_brace = true
|
||||
ij_groovy_space_before_colon = true
|
||||
ij_groovy_space_before_comma = false
|
||||
ij_groovy_space_before_do_left_brace = true
|
||||
ij_groovy_space_before_else_keyword = true
|
||||
ij_groovy_space_before_else_left_brace = true
|
||||
ij_groovy_space_before_finally_keyword = true
|
||||
ij_groovy_space_before_finally_left_brace = true
|
||||
ij_groovy_space_before_for_left_brace = true
|
||||
ij_groovy_space_before_for_parentheses = true
|
||||
ij_groovy_space_before_for_semicolon = false
|
||||
ij_groovy_space_before_if_left_brace = true
|
||||
ij_groovy_space_before_if_parentheses = true
|
||||
ij_groovy_space_before_method_call_parentheses = false
|
||||
ij_groovy_space_before_method_left_brace = true
|
||||
ij_groovy_space_before_method_parentheses = false
|
||||
ij_groovy_space_before_quest = true
|
||||
ij_groovy_space_before_switch_left_brace = true
|
||||
ij_groovy_space_before_switch_parentheses = true
|
||||
ij_groovy_space_before_synchronized_left_brace = true
|
||||
ij_groovy_space_before_synchronized_parentheses = true
|
||||
ij_groovy_space_before_try_left_brace = true
|
||||
ij_groovy_space_before_try_parentheses = true
|
||||
ij_groovy_space_before_while_keyword = true
|
||||
ij_groovy_space_before_while_left_brace = true
|
||||
ij_groovy_space_before_while_parentheses = true
|
||||
ij_groovy_space_in_named_argument = true
|
||||
ij_groovy_space_in_named_argument_before_colon = false
|
||||
ij_groovy_space_within_empty_array_initializer_braces = false
|
||||
ij_groovy_space_within_empty_method_call_parentheses = false
|
||||
ij_groovy_spaces_around_additive_operators = true
|
||||
ij_groovy_spaces_around_assignment_operators = true
|
||||
ij_groovy_spaces_around_bitwise_operators = true
|
||||
ij_groovy_spaces_around_equality_operators = true
|
||||
ij_groovy_spaces_around_lambda_arrow = true
|
||||
ij_groovy_spaces_around_logical_operators = true
|
||||
ij_groovy_spaces_around_multiplicative_operators = true
|
||||
ij_groovy_spaces_around_regex_operators = true
|
||||
ij_groovy_spaces_around_relational_operators = true
|
||||
ij_groovy_spaces_around_shift_operators = true
|
||||
ij_groovy_spaces_within_annotation_parentheses = false
|
||||
ij_groovy_spaces_within_array_initializer_braces = false
|
||||
ij_groovy_spaces_within_braces = true
|
||||
ij_groovy_spaces_within_brackets = false
|
||||
ij_groovy_spaces_within_cast_parentheses = false
|
||||
ij_groovy_spaces_within_catch_parentheses = false
|
||||
ij_groovy_spaces_within_for_parentheses = false
|
||||
ij_groovy_spaces_within_gstring_injection_braces = false
|
||||
ij_groovy_spaces_within_if_parentheses = false
|
||||
ij_groovy_spaces_within_list_or_map = false
|
||||
ij_groovy_spaces_within_method_call_parentheses = false
|
||||
ij_groovy_spaces_within_method_parentheses = false
|
||||
ij_groovy_spaces_within_parentheses = false
|
||||
ij_groovy_spaces_within_switch_parentheses = false
|
||||
ij_groovy_spaces_within_synchronized_parentheses = false
|
||||
ij_groovy_spaces_within_try_parentheses = false
|
||||
ij_groovy_spaces_within_tuple_expression = false
|
||||
ij_groovy_spaces_within_while_parentheses = false
|
||||
ij_groovy_special_else_if_treatment = true
|
||||
ij_groovy_ternary_operation_wrap = off
|
||||
ij_groovy_throws_keyword_wrap = off
|
||||
ij_groovy_throws_list_wrap = off
|
||||
ij_groovy_use_flying_geese_braces = false
|
||||
ij_groovy_use_fq_class_names = false
|
||||
ij_groovy_use_fq_class_names_in_javadoc = true
|
||||
ij_groovy_use_relative_indents = false
|
||||
ij_groovy_use_single_class_imports = true
|
||||
ij_groovy_variable_annotation_wrap = off
|
||||
ij_groovy_while_brace_force = never
|
||||
ij_groovy_while_on_new_line = false
|
||||
ij_groovy_wrap_long_lines = false
|
||||
|
||||
[{*.gradle.kts,*.kt,*.kts,*.main.kts,*.space.kts}]
|
||||
insert_final_newline = true
|
||||
max_line_length = 100
|
||||
indent_size = 4
|
||||
ij_continuation_indent_size = 4 # was 8
|
||||
ij_java_names_count_to_use_import_on_demand = 9999
|
||||
ij_kotlin_align_in_columns_case_branch = false
|
||||
ij_kotlin_align_multiline_binary_operation = false
|
||||
ij_kotlin_align_multiline_extends_list = false
|
||||
ij_kotlin_align_multiline_method_parentheses = false
|
||||
ij_kotlin_align_multiline_parameters = true
|
||||
ij_kotlin_align_multiline_parameters_in_calls = false
|
||||
ij_kotlin_allow_trailing_comma = false
|
||||
ij_kotlin_allow_trailing_comma_on_call_site = false
|
||||
ij_kotlin_assignment_wrap = off
|
||||
ij_kotlin_allow_trailing_comma = true
|
||||
ij_kotlin_allow_trailing_comma_on_call_site = true
|
||||
ij_kotlin_assignment_wrap = normal
|
||||
ij_kotlin_blank_lines_after_class_header = 0
|
||||
ij_kotlin_blank_lines_around_block_when_branches = 0
|
||||
ij_kotlin_blank_lines_before_declaration_with_comment_or_annotation_on_separate_line = 1
|
||||
ij_kotlin_block_comment_at_first_column = true
|
||||
ij_kotlin_call_parameters_new_line_after_left_paren = false
|
||||
ij_kotlin_call_parameters_new_line_after_left_paren = true
|
||||
ij_kotlin_call_parameters_right_paren_on_new_line = false
|
||||
ij_kotlin_call_parameters_wrap = off
|
||||
ij_kotlin_call_parameters_wrap = on_every_item
|
||||
ij_kotlin_catch_on_new_line = false
|
||||
ij_kotlin_class_annotation_wrap = split_into_lines
|
||||
ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL
|
||||
ij_kotlin_continuation_indent_for_chained_calls = true
|
||||
ij_kotlin_continuation_indent_for_expression_bodies = true
|
||||
ij_kotlin_continuation_indent_in_argument_lists = true
|
||||
ij_kotlin_continuation_indent_in_elvis = true
|
||||
ij_kotlin_continuation_indent_in_if_conditions = true
|
||||
ij_kotlin_continuation_indent_in_parameter_lists = true
|
||||
ij_kotlin_continuation_indent_in_supertype_lists = true
|
||||
ij_kotlin_continuation_indent_in_elvis = false
|
||||
ij_kotlin_continuation_indent_in_if_conditions = false
|
||||
ij_kotlin_continuation_indent_in_parameter_lists = false
|
||||
ij_kotlin_continuation_indent_in_supertype_lists = false
|
||||
ij_kotlin_else_on_new_line = false
|
||||
ij_kotlin_enum_constants_wrap = off
|
||||
ij_kotlin_extends_list_wrap = off
|
||||
ij_kotlin_extends_list_wrap = normal
|
||||
ij_kotlin_field_annotation_wrap = split_into_lines
|
||||
ij_kotlin_finally_on_new_line = false
|
||||
ij_kotlin_if_rparen_on_new_line = false
|
||||
ij_kotlin_import_nested_classes = false
|
||||
ij_kotlin_imports_layout = *,java.**,javax.**,kotlin.**,^
|
||||
ij_kotlin_insert_whitespaces_in_simple_one_line_method = true
|
||||
ij_kotlin_keep_blank_lines_before_right_brace = 2
|
||||
ij_kotlin_keep_blank_lines_in_code = 2
|
||||
|
@ -519,13 +57,13 @@ ij_kotlin_lbrace_on_next_line = false
|
|||
ij_kotlin_line_comment_add_space = false
|
||||
ij_kotlin_line_comment_at_first_column = true
|
||||
ij_kotlin_method_annotation_wrap = split_into_lines
|
||||
ij_kotlin_method_call_chain_wrap = off
|
||||
ij_kotlin_method_parameters_new_line_after_left_paren = false
|
||||
ij_kotlin_method_parameters_right_paren_on_new_line = false
|
||||
ij_kotlin_method_parameters_wrap = off
|
||||
ij_kotlin_name_count_to_use_star_import = 5
|
||||
ij_kotlin_name_count_to_use_star_import_for_members = 3
|
||||
ij_kotlin_packages_to_use_import_on_demand = java.util.*,kotlinx.android.synthetic.**,io.ktor.**
|
||||
ij_kotlin_method_call_chain_wrap = normal
|
||||
ij_kotlin_method_parameters_new_line_after_left_paren = true
|
||||
ij_kotlin_method_parameters_right_paren_on_new_line = true
|
||||
ij_kotlin_method_parameters_wrap = on_every_item
|
||||
ij_kotlin_name_count_to_use_star_import = 9999
|
||||
ij_kotlin_name_count_to_use_star_import_for_members = 9999
|
||||
ij_java_names_count_to_use_import_on_demand = 9999
|
||||
ij_kotlin_parameter_annotation_wrap = off
|
||||
ij_kotlin_space_after_comma = true
|
||||
ij_kotlin_space_after_extend_colon = true
|
||||
|
@ -552,72 +90,5 @@ ij_kotlin_spaces_around_when_arrow = true
|
|||
ij_kotlin_variable_annotation_wrap = off
|
||||
ij_kotlin_while_on_new_line = false
|
||||
ij_kotlin_wrap_elvis_expressions = 1
|
||||
ij_kotlin_wrap_expression_body_functions = 0
|
||||
ij_kotlin_wrap_expression_body_functions = 1
|
||||
ij_kotlin_wrap_first_method_in_call_chain = false
|
||||
|
||||
[{*.har,*.json}]
|
||||
indent_size = 2
|
||||
ij_json_keep_blank_lines_in_code = 0
|
||||
ij_json_keep_indents_on_empty_lines = false
|
||||
ij_json_keep_line_breaks = true
|
||||
ij_json_space_after_colon = true
|
||||
ij_json_space_after_comma = true
|
||||
ij_json_space_before_colon = true
|
||||
ij_json_space_before_comma = false
|
||||
ij_json_spaces_within_braces = false
|
||||
ij_json_spaces_within_brackets = false
|
||||
ij_json_wrap_long_lines = false
|
||||
|
||||
[{*.htm,*.html,*.sht,*.shtm,*.shtml}]
|
||||
ij_html_add_new_line_before_tags = body,div,p,form,h1,h2,h3
|
||||
ij_html_align_attributes = true
|
||||
ij_html_align_text = false
|
||||
ij_html_attribute_wrap = normal
|
||||
ij_html_block_comment_at_first_column = true
|
||||
ij_html_do_not_align_children_of_min_lines = 0
|
||||
ij_html_do_not_break_if_inline_tags = title,h1,h2,h3,h4,h5,h6,p
|
||||
ij_html_do_not_indent_children_of_tags = html,body,thead,tbody,tfoot
|
||||
ij_html_enforce_quotes = false
|
||||
ij_html_inline_tags = a,abbr,acronym,b,basefont,bdo,big,br,cite,cite,code,dfn,em,font,i,img,input,kbd,label,q,s,samp,select,small,span,strike,strong,sub,sup,textarea,tt,u,var
|
||||
ij_html_keep_blank_lines = 2
|
||||
ij_html_keep_indents_on_empty_lines = false
|
||||
ij_html_keep_line_breaks = true
|
||||
ij_html_keep_line_breaks_in_text = true
|
||||
ij_html_keep_whitespaces = false
|
||||
ij_html_keep_whitespaces_inside = span,pre,textarea
|
||||
ij_html_line_comment_at_first_column = true
|
||||
ij_html_new_line_after_last_attribute = never
|
||||
ij_html_new_line_before_first_attribute = never
|
||||
ij_html_quote_style = double
|
||||
ij_html_remove_new_line_before_tags = br
|
||||
ij_html_space_after_tag_name = false
|
||||
ij_html_space_around_equality_in_attribute = false
|
||||
ij_html_space_inside_empty_tag = false
|
||||
ij_html_text_wrap = normal
|
||||
ij_html_uniform_ident = false
|
||||
|
||||
[{*.markdown,*.md}]
|
||||
ij_markdown_force_one_space_after_blockquote_symbol = true
|
||||
ij_markdown_force_one_space_after_header_symbol = true
|
||||
ij_markdown_force_one_space_after_list_bullet = true
|
||||
ij_markdown_force_one_space_between_words = true
|
||||
ij_markdown_keep_indents_on_empty_lines = false
|
||||
ij_markdown_max_lines_around_block_elements = 1
|
||||
ij_markdown_max_lines_around_header = 1
|
||||
ij_markdown_max_lines_between_paragraphs = 1
|
||||
ij_markdown_min_lines_around_block_elements = 1
|
||||
ij_markdown_min_lines_around_header = 1
|
||||
ij_markdown_min_lines_between_paragraphs = 1
|
||||
|
||||
[{*.yaml,*.yml}]
|
||||
indent_size = 2
|
||||
ij_yaml_align_values_properties = do_not_align
|
||||
ij_yaml_autoinsert_sequence_marker = true
|
||||
ij_yaml_block_mapping_on_new_line = false
|
||||
ij_yaml_indent_sequence_value = true
|
||||
ij_yaml_keep_indents_on_empty_lines = false
|
||||
ij_yaml_keep_line_breaks = true
|
||||
ij_yaml_sequence_on_new_line = false
|
||||
ij_yaml_space_before_colon = false
|
||||
ij_yaml_spaces_within_braces = true
|
||||
ij_yaml_spaces_within_brackets = true
|
||||
|
|
2
.git-blame-ignore-revs
Normal file
2
.git-blame-ignore-revs
Normal file
|
@ -0,0 +1,2 @@
|
|||
# Ignore initial spotlessApply using ktfmt
|
||||
51e9bfc67f19e16a69790a8d92bd6b1c86a76a5f
|
10
.reuse/dep5
10
.reuse/dep5
|
@ -9,6 +9,11 @@ Source: https://pgpainless.org
|
|||
# Copyright: $YEAR $NAME <$CONTACT>
|
||||
# License: ...
|
||||
|
||||
# GitBlameIgnore
|
||||
Files: .git-blame-ignore-revs
|
||||
Copyright: 2023 Paul Schaub <info@pgpainless.org>
|
||||
License: CC0-1.0
|
||||
|
||||
# Documentation
|
||||
Files: docs/*
|
||||
Copyright: 2022 Paul Schaub <info@pgpainless.org>
|
||||
|
@ -23,6 +28,11 @@ Files: gradle*
|
|||
Copyright: 2015 the original author or authors.
|
||||
License: Apache-2.0
|
||||
|
||||
# Editorconfig
|
||||
Files: .editorconfig
|
||||
Copyright: Facebook
|
||||
License: Apache-2.0
|
||||
|
||||
# PGPainless Logo
|
||||
Files: assets/repository-open-graph.png
|
||||
Copyright: 2021 Paul Schaub <info@pgpainless.org>
|
||||
|
|
30
build.gradle
30
build.gradle
|
@ -18,7 +18,8 @@ buildscript {
|
|||
}
|
||||
|
||||
plugins {
|
||||
id 'ru.vyarus.animalsniffer' version '1.5.3'
|
||||
id 'org.jetbrains.kotlin.jvm' version "1.8.10"
|
||||
id 'com.diffplug.spotless' version '6.22.0' apply false
|
||||
}
|
||||
|
||||
apply from: 'version.gradle'
|
||||
|
@ -29,6 +30,8 @@ allprojects {
|
|||
apply plugin: 'eclipse'
|
||||
apply plugin: 'jacoco'
|
||||
apply plugin: 'checkstyle'
|
||||
apply plugin: 'kotlin'
|
||||
apply plugin: 'com.diffplug.spotless'
|
||||
|
||||
compileJava {
|
||||
options.release = 8
|
||||
|
@ -41,23 +44,17 @@ allprojects {
|
|||
onlyIf { !sourceSets.main.allSource.files.isEmpty() }
|
||||
}
|
||||
|
||||
// For library modules, enable android api compatibility check
|
||||
if (it.name != 'pgpainless-cli') {
|
||||
// animalsniffer
|
||||
apply plugin: 'ru.vyarus.animalsniffer'
|
||||
dependencies {
|
||||
signature "net.sf.androidscents.signature:android-api-level-${pgpainlessMinAndroidSdk}:2.3.3_r2@signature"
|
||||
}
|
||||
animalsniffer {
|
||||
sourceSets = [sourceSets.main]
|
||||
}
|
||||
}
|
||||
|
||||
// checkstyle
|
||||
checkstyle {
|
||||
toolVersion = '10.12.1'
|
||||
}
|
||||
|
||||
spotless {
|
||||
kotlin {
|
||||
ktfmt().dropboxStyle()
|
||||
}
|
||||
}
|
||||
|
||||
group 'org.pgpainless'
|
||||
description = "Simple to use OpenPGP API for Java based on Bouncycastle"
|
||||
version = shortVersion
|
||||
|
@ -78,6 +75,13 @@ allprojects {
|
|||
fileMode = 0644
|
||||
}
|
||||
|
||||
// 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()
|
||||
|
|
|
@ -1,95 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package org.bouncycastle;
|
||||
|
||||
import org.bouncycastle.openpgp.PGPException;
|
||||
import org.bouncycastle.openpgp.PGPPrivateKey;
|
||||
import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory;
|
||||
import org.bouncycastle.openpgp.operator.bc.BcPublicKeyDataDecryptorFactory;
|
||||
import org.bouncycastle.util.encoders.Base64;
|
||||
import org.bouncycastle.util.encoders.Hex;
|
||||
import org.pgpainless.decryption_verification.CustomPublicKeyDataDecryptorFactory;
|
||||
import org.pgpainless.key.SubkeyIdentifier;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Implementation of the {@link PublicKeyDataDecryptorFactory} which caches decrypted session keys.
|
||||
* That way, if a message needs to be decrypted multiple times, expensive private key operations can be omitted.
|
||||
*
|
||||
* This implementation changes the behavior or {@link #recoverSessionData(int, byte[][])} to first return any
|
||||
* cache hits.
|
||||
* If no hit is found, the method call is delegated to the underlying {@link PublicKeyDataDecryptorFactory}.
|
||||
* The result of that is then placed in the cache and returned.
|
||||
*/
|
||||
public class CachingBcPublicKeyDataDecryptorFactory
|
||||
extends BcPublicKeyDataDecryptorFactory
|
||||
implements CustomPublicKeyDataDecryptorFactory {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(CachingBcPublicKeyDataDecryptorFactory.class);
|
||||
|
||||
private final Map<String, byte[]> 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;
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
/**
|
||||
* Classes which could be upstreamed to BC at some point.
|
||||
*/
|
||||
package org.bouncycastle;
|
|
@ -1,238 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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();
|
||||
}
|
||||
}
|
|
@ -1,100 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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 <a href="https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-06.html#name-aead-algorithms">
|
||||
* Crypto-Refresh-06 §9.6 - AEAD Algorithms</a>
|
||||
*/
|
||||
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<Integer, AEADAlgorithm> 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,63 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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<SymmetricKeyAlgorithm> symmetricKeyAlgorithms;
|
||||
private final Set<HashAlgorithm> hashAlgorithms;
|
||||
private final Set<CompressionAlgorithm> compressionAlgorithms;
|
||||
|
||||
public AlgorithmSuite(List<SymmetricKeyAlgorithm> symmetricKeyAlgorithms,
|
||||
List<HashAlgorithm> hashAlgorithms,
|
||||
List<CompressionAlgorithm> compressionAlgorithms) {
|
||||
this.symmetricKeyAlgorithms = Collections.unmodifiableSet(new LinkedHashSet<>(symmetricKeyAlgorithms));
|
||||
this.hashAlgorithms = Collections.unmodifiableSet(new LinkedHashSet<>(hashAlgorithms));
|
||||
this.compressionAlgorithms = Collections.unmodifiableSet(new LinkedHashSet<>(compressionAlgorithms));
|
||||
}
|
||||
|
||||
public Set<SymmetricKeyAlgorithm> getSymmetricKeyAlgorithms() {
|
||||
return new LinkedHashSet<>(symmetricKeyAlgorithms);
|
||||
}
|
||||
|
||||
public Set<HashAlgorithm> getHashAlgorithms() {
|
||||
return new LinkedHashSet<>(hashAlgorithms);
|
||||
}
|
||||
|
||||
public Set<CompressionAlgorithm> getCompressionAlgorithms() {
|
||||
return new LinkedHashSet<>(compressionAlgorithms);
|
||||
}
|
||||
|
||||
public static AlgorithmSuite getDefaultAlgorithmSuite() {
|
||||
return defaultAlgorithmSuite;
|
||||
}
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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;
|
||||
}
|
||||
}
|
|
@ -1,79 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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 <a href="https://tools.ietf.org/html/rfc4880#section-9.3">RFC4880: Compression Algorithm Tags</a>
|
||||
*/
|
||||
public enum CompressionAlgorithm {
|
||||
|
||||
UNCOMPRESSED (CompressionAlgorithmTags.UNCOMPRESSED),
|
||||
ZIP (CompressionAlgorithmTags.ZIP),
|
||||
ZLIB (CompressionAlgorithmTags.ZLIB),
|
||||
BZIP2 (CompressionAlgorithmTags.BZIP2),
|
||||
;
|
||||
|
||||
private static final Map<Integer, CompressionAlgorithm> 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;
|
||||
}
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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
|
||||
* <pre>
|
||||
* {@code <CR><LF>}
|
||||
* </pre>.
|
||||
*/
|
||||
CANONICAL_TEXT_DOCUMENT(SignatureType.CANONICAL_TEXT_DOCUMENT),
|
||||
;
|
||||
|
||||
final SignatureType signatureType;
|
||||
|
||||
DocumentSignatureType(SignatureType signatureType) {
|
||||
this.signatureType = signatureType;
|
||||
}
|
||||
|
||||
public SignatureType getSignatureType() {
|
||||
return signatureType;
|
||||
}
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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
|
||||
}
|
|
@ -1,152 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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 <a href="https://tools.ietf.org/html/rfc4880#section-5.2.3.24">RFC4880: Features</a>
|
||||
*/
|
||||
public enum Feature {
|
||||
|
||||
/**
|
||||
* Support for Symmetrically Encrypted Integrity Protected Data Packets (version 1) using Modification
|
||||
* Detection Code Packets.
|
||||
*
|
||||
* @see <a href="https://tools.ietf.org/html/rfc4880#section-5.14">
|
||||
* RFC-4880 §5.14: Modification Detection Code Packet</a>
|
||||
*/
|
||||
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 <a href="https://openpgp-wg.gitlab.io/rfc4880bis/#name-aead-encrypted-data-packet-">
|
||||
* AEAD Encrypted Data Packet</a>
|
||||
*/
|
||||
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 <a href="https://openpgp-wg.gitlab.io/rfc4880bis/#name-public-key-packet-formats">
|
||||
* Public-Key Packet Formats</a>
|
||||
*/
|
||||
GNUPG_VERSION_5_PUBLIC_KEY(Features.FEATURE_VERSION_5_PUBLIC_KEY),
|
||||
|
||||
/**
|
||||
* Support for Symmetrically Encrypted Integrity Protected Data packet version 2.
|
||||
*
|
||||
* @see <a href="https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-06.html#version-two-seipd">
|
||||
* crypto-refresh-06 §5.13.2. Version 2 Sym. Encrypted Integrity Protected Data Packet Format</a>
|
||||
*/
|
||||
MODIFICATION_DETECTION_2((byte) 0x08),
|
||||
;
|
||||
|
||||
private static final Map<Byte, Feature> 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<Feature> fromBitmask(int bitmask) {
|
||||
List<Feature> 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;
|
||||
}
|
||||
}
|
|
@ -1,118 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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 <a href="https://tools.ietf.org/html/rfc4880#section-9.4">RFC4880: Hash Algorithms</a>
|
||||
*/
|
||||
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<Integer, HashAlgorithm> ID_MAP = new HashMap<>();
|
||||
private static final Map<String, HashAlgorithm> 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 <a href="https://datatracker.ietf.org/doc/html/rfc4880#section-9.4">RFC4880: §9.4 Hash Algorithms</a>
|
||||
* 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;
|
||||
}
|
||||
}
|
|
@ -1,121 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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 <a href="https://tools.ietf.org/html/rfc4880#section-5.2.3.21">RFC4880: Key Flags</a>
|
||||
*/
|
||||
public enum KeyFlag {
|
||||
|
||||
/**
|
||||
* This key may be used to certify third-party 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<KeyFlag> fromBitmask(int bitmask) {
|
||||
List<KeyFlag> 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;
|
||||
}
|
||||
}
|
|
@ -1,71 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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<Integer, OpenPgpPacket> 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;
|
||||
}
|
||||
}
|
|
@ -1,163 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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 <a href="https://tools.ietf.org/html/rfc4880#section-9.1">RFC4880: Public-Key Algorithms</a>
|
||||
*/
|
||||
public enum PublicKeyAlgorithm {
|
||||
|
||||
/**
|
||||
* RSA capable of encryption and signatures.
|
||||
*/
|
||||
RSA_GENERAL (PublicKeyAlgorithmTags.RSA_GENERAL, true, true),
|
||||
|
||||
/**
|
||||
* RSA with usage encryption.
|
||||
*
|
||||
* @deprecated see <a href="https://tools.ietf.org/html/rfc4880#section-13.5">Deprecation notice</a>
|
||||
*/
|
||||
@Deprecated
|
||||
RSA_ENCRYPT (PublicKeyAlgorithmTags.RSA_ENCRYPT, false, true),
|
||||
|
||||
/**
|
||||
* RSA with usage of creating signatures.
|
||||
*
|
||||
* @deprecated see <a href="https://tools.ietf.org/html/rfc4880#section-13.5">Deprecation notice</a>
|
||||
*/
|
||||
@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 <a href="https://tools.ietf.org/html/rfc4880#section-13.8">Deprecation notice</a>
|
||||
*/
|
||||
@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<Integer, PublicKeyAlgorithm> 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;
|
||||
}
|
||||
}
|
|
@ -1,131 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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<RevocationState> {
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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
|
||||
}
|
|
@ -1,462 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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 <a href="https://tools.ietf.org/html/rfc4880#section-5.2.3.1">RFC4880: Signature Subpacket Specification</a>
|
||||
*/
|
||||
public enum SignatureSubpacket {
|
||||
|
||||
/**
|
||||
* The time the signature was made.
|
||||
* MUST be present in the hashed area of the signature.
|
||||
*
|
||||
* @see <a href="https://tools.ietf.org/html/rfc4880#section-5.2.3.4">Signature Creation Time</a>
|
||||
*/
|
||||
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 <a href="https://tools.ietf.org/html/rfc4880#section-5.2.3.10">Signature Expiration Time</a>
|
||||
*/
|
||||
signatureExpirationTime(EXPIRE_TIME),
|
||||
|
||||
/**
|
||||
* Denotes whether the signature is exportable for other users.
|
||||
*
|
||||
* @see <a href="https://tools.ietf.org/html/rfc4880#section-5.2.3.11">Exportable Certification</a>
|
||||
*/
|
||||
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 <a href="https://tools.ietf.org/html/rfc4880#section-5.2.3.13">Trust Signature</a>
|
||||
*/
|
||||
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 <a href="https://tools.ietf.org/html/rfc4880#section-5.2.3.14">Regular Expression</a>
|
||||
*/
|
||||
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 <a href="https://tools.ietf.org/html/rfc4880#section-5.2.3.12">Revocable</a>
|
||||
*/
|
||||
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 <a href="https://tools.ietf.org/html/rfc4880#section-5.2.3.6">Key Expiration Time</a>
|
||||
*/
|
||||
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 <a href="https://tools.ietf.org/html/rfc4880#section-5.2.3.7">Preferred Symmetric Algorithms</a>
|
||||
*/
|
||||
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 <a href="https://tools.ietf.org/html/rfc4880#section-5.2.3.15">Revocation Key</a>
|
||||
*/
|
||||
revocationKey(REVOCATION_KEY),
|
||||
|
||||
/**
|
||||
* The OpenPGP Key ID of the key issuing the signature.
|
||||
*
|
||||
* @see <a href="https://tools.ietf.org/html/rfc4880#section-5.2.3.5">Issuer Key ID</a>
|
||||
*/
|
||||
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 <a href="https://tools.ietf.org/html/rfc4880#section-5.2.3.16">Notation Data</a>
|
||||
*/
|
||||
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 <a href="https://tools.ietf.org/html/rfc4880#section-5.2.3.8">Preferred Hash Algorithms</a>
|
||||
*/
|
||||
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 <a href="https://tools.ietf.org/html/rfc4880#section-5.2.3.9">Preferred Compressio Algorithms</a>
|
||||
*/
|
||||
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 <a href="https://tools.ietf.org/html/rfc4880#section-5.2.3.17">Key Server Preferences</a>
|
||||
*/
|
||||
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 <a href="https://tools.ietf.org/html/rfc4880#section-5.2.3.18">Preferred Key Server</a>
|
||||
*/
|
||||
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 <a href="https://tools.ietf.org/html/rfc4880#section-5.2.3.19">Primary User-ID</a>
|
||||
*/
|
||||
primaryUserId(PRIMARY_USER_ID),
|
||||
|
||||
/**
|
||||
* This subpacket contains a URI of a document that describes the policy
|
||||
* under which the signature was issued.
|
||||
*
|
||||
* @see <a href="https://tools.ietf.org/html/rfc4880#section-5.2.3.20">Policy URL</a>
|
||||
*/
|
||||
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 <a href="https://tools.ietf.org/html/rfc4880#section-5.2.3.21">Key Flags</a>
|
||||
*/
|
||||
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 <a href="https://tools.ietf.org/html/rfc4880#section-5.2.3.22">Signer's User ID</a>
|
||||
*/
|
||||
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 <a href="https://tools.ietf.org/html/rfc4880#section-5.2.3.23">Reason for Revocation</a>
|
||||
*/
|
||||
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 <a href="https://tools.ietf.org/html/rfc4880#section-5.2.3.24">Features</a>
|
||||
*/
|
||||
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 <a href="https://tools.ietf.org/html/rfc4880#section-5.2.3.25">Signature Target</a>
|
||||
*/
|
||||
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 <a href="https://tools.ietf.org/html/rfc4880#section-5.2.3.26">Embedded Signature</a>
|
||||
*/
|
||||
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 <a href="https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-10#section-5.2.3.28">Issuer Fingerprint</a>
|
||||
*/
|
||||
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 <a href="https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-10#section-5.2.3.8">Preferred AEAD Algorithms</a>
|
||||
*/
|
||||
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 <a href="https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-10#section-5.2.3.29">Intended Recipient Fingerprint</a>
|
||||
*/
|
||||
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 <a href="https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-10#section-5.2.3.30">Attested Certification</a>
|
||||
*/
|
||||
attestedCertification(ATTESTED_CERTIFICATIONS)
|
||||
;
|
||||
|
||||
private static final Map<Integer, SignatureSubpacket> 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<SignatureSubpacket> fromCodes(int[] codes) {
|
||||
List<SignatureSubpacket> tags = new ArrayList<>();
|
||||
for (int code : codes) {
|
||||
try {
|
||||
tags.add(requireFromCode(code));
|
||||
} catch (NoSuchElementException e) {
|
||||
// skip
|
||||
}
|
||||
}
|
||||
return tags;
|
||||
}
|
||||
}
|
|
@ -1,225 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2020 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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 <a href="https://tools.ietf.org/html/rfc4880#section-5.11">rfc4880 §5.2.1. Signature Types</a>
|
||||
*/
|
||||
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 <CR><LF>}.
|
||||
*/
|
||||
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<Integer, SignatureType> 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,101 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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 <a href="https://tools.ietf.org/html/rfc4880#section-5.9">RFC4880: Literal Data Packet</a>
|
||||
*/
|
||||
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<Character, StreamEncoding> 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;
|
||||
}
|
||||
}
|
|
@ -1,150 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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 <a href="https://tools.ietf.org/html/rfc4880#section-9.2">RFC4880: Symmetric-Key Algorithms</a>
|
||||
*/
|
||||
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<Integer, SymmetricKeyAlgorithm> 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;
|
||||
}
|
||||
}
|
|
@ -1,188 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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 <pre>otherDepth</pre>.
|
||||
*
|
||||
* @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 <pre>other</pre> 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 <pre>n</pre>.
|
||||
* This key can introduce meta introducers of depth <pre>n - 1</pre>.
|
||||
*
|
||||
* @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;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,70 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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<HashAlgorithm> 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<HashAlgorithm> orderedPreferencesSet) {
|
||||
for (HashAlgorithm preference : orderedPreferencesSet) {
|
||||
if (hashAlgorithmPolicy.isAcceptable(preference)) {
|
||||
return preference;
|
||||
}
|
||||
}
|
||||
return hashAlgorithmPolicy.defaultHashAlgorithm();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
|
@ -1,105 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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<Set<SymmetricKeyAlgorithm>> 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<Set<SymmetricKeyAlgorithm>> 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<SymmetricKeyAlgorithm, Integer> supportWeight = new LinkedHashMap<>();
|
||||
for (Set<SymmetricKeyAlgorithm> 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<Integer, List<SymmetricKeyAlgorithm>> byScore = new HashMap<>();
|
||||
for (SymmetricKeyAlgorithm algorithm : supportWeight.keySet()) {
|
||||
int score = supportWeight.get(algorithm);
|
||||
List<SymmetricKeyAlgorithm> withSameScore = byScore.get(score);
|
||||
if (withSameScore == null) {
|
||||
withSameScore = new ArrayList<>();
|
||||
byScore.put(score, withSameScore);
|
||||
}
|
||||
withSameScore.add(algorithm);
|
||||
}
|
||||
|
||||
List<Integer> 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<SymmetricKeyAlgorithm> 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();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
/**
|
||||
* Classes related to algorithm negotiation.
|
||||
*/
|
||||
package org.pgpainless.algorithm.negotiation;
|
|
@ -1,8 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
/**
|
||||
* Enums which map to OpenPGP's algorithm IDs.
|
||||
*/
|
||||
package org.pgpainless.algorithm;
|
|
@ -1,139 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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<CertificationChain, Integer> certificationChains = new HashMap<>();
|
||||
private final int targetAmount;
|
||||
|
||||
public CertificateAuthenticity(@Nonnull PGPPublicKeyRing certificate,
|
||||
@Nonnull String userId,
|
||||
@Nonnull Map<CertificationChain, Integer> 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<CertificationChain, Integer> getCertificationChains() {
|
||||
return Collections.unmodifiableMap(certificationChains);
|
||||
}
|
||||
|
||||
public static class CertificationChain {
|
||||
private final int trustAmount;
|
||||
private final List<ChainLink> 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<ChainLink> 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<ChainLink> 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,71 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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 <a href="https://github.com/pgpainless/pgpainless-wot">PGPainless-WOT</a>
|
||||
* @see <a href="https://sequoia-pgp.gitlab.io/sequoia-wot/">OpenPGP Web of Trust</a>
|
||||
*/
|
||||
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<CertificateAuthenticity> 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<CertificateAuthenticity> identifyByFingerprint(@Nonnull OpenPgpFingerprint fingerprint,
|
||||
@Nonnull Date referenceTime,
|
||||
int targetAmount);
|
||||
}
|
|
@ -1,508 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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<PGPSignature> detachedSignatures = new HashSet<>();
|
||||
private MissingPublicKeyCallback missingCertificateCallback = null;
|
||||
|
||||
// Session key for decryption without passphrase/key
|
||||
private SessionKey sessionKey = null;
|
||||
private final Map<SubkeyIdentifier, PublicKeyDataDecryptorFactory> customPublicKeyDataDecryptorFactories =
|
||||
new HashMap<>();
|
||||
|
||||
private final Map<PGPSecretKeyRing, SecretKeyRingProtector> decryptionKeys = new HashMap<>();
|
||||
private final Set<Passphrase> 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<PGPSignature> signatures = SignatureUtils.readSignatures(signatureInputStream);
|
||||
return addVerificationOfDetachedSignatures(signatures);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add some detached signatures for verification.
|
||||
*
|
||||
* @param detachedSignatures detached signatures
|
||||
* @return options
|
||||
*/
|
||||
public ConsumerOptions addVerificationOfDetachedSignatures(List<PGPSignature> 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 <a href="https://datatracker.ietf.org/doc/html/rfc4880#section-2.1">RFC4880 on Session Keys</a>
|
||||
*
|
||||
* @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 <a href="https://datatracker.ietf.org/doc/html/rfc4880#section-5.7">Symmetrically Encrypted Data Packet</a>
|
||||
*
|
||||
* @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<SubkeyIdentifier, PublicKeyDataDecryptorFactory> getCustomDecryptorFactories() {
|
||||
return new HashMap<>(customPublicKeyDataDecryptorFactories);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the set of available decryption keys.
|
||||
*
|
||||
* @return decryption keys
|
||||
*/
|
||||
public @Nonnull Set<PGPSecretKeyRing> getDecryptionKeys() {
|
||||
return Collections.unmodifiableSet(decryptionKeys.keySet());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the set of available message decryption passphrases.
|
||||
*
|
||||
* @return decryption passphrases
|
||||
*/
|
||||
public @Nonnull Set<Passphrase> getDecryptionPassphrases() {
|
||||
return Collections.unmodifiableSet(decryptionPassphrases);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the explicitly set verification certificates.
|
||||
*
|
||||
* @deprecated use {@link #getCertificateSource()} instead.
|
||||
* @return verification certs
|
||||
*/
|
||||
@Deprecated
|
||||
public @Nonnull Set<PGPPublicKeyRing> 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 <pre>null</pre> 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<PGPSignature> 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 <pre>ignoreMDCErrors</pre> is set to true, PGPainless will
|
||||
* <ul>
|
||||
* <li>not throw exceptions for SEIP packets with tampered ciphertext</li>
|
||||
* <li>not throw exceptions for SEIP packets with tampered MDC</li>
|
||||
* <li>not throw exceptions for MDCs with bad CTB</li>
|
||||
* <li>not throw exceptions for MDCs with bad length</li>
|
||||
* </ul>
|
||||
*
|
||||
* It will however still throw an exception if it encounters a SEIP packet with missing or truncated MDC
|
||||
*
|
||||
* @see <a href="https://datatracker.ietf.org/doc/html/rfc4880#section-5.13">
|
||||
* Sym. Encrypted Integrity Protected Data Packet</a>
|
||||
* @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<PGPPublicKeyRing> 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<PGPPublicKeyRing> 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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();
|
||||
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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;
|
||||
|
||||
}
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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();
|
||||
}
|
||||
}
|
|
@ -1,105 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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 {
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -1,60 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,146 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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<Long> 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<Long> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,858 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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<EncryptedData> 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<Long> getRecipientKeyIds() {
|
||||
List<Long> keyIds = new ArrayList<>();
|
||||
Iterator<EncryptedData> encLayers = getEncryptionLayers();
|
||||
while (encLayers.hasNext()) {
|
||||
EncryptedData layer = encLayers.next();
|
||||
keyIds.addAll(layer.getRecipients());
|
||||
}
|
||||
return keyIds;
|
||||
}
|
||||
|
||||
public @Nonnull Iterator<EncryptedData> getEncryptionLayers() {
|
||||
return new LayerIterator<EncryptedData>(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<SymmetricKeyAlgorithm> getEncryptionAlgorithms() {
|
||||
return map(getEncryptionLayers(), encryptedData -> encryptedData.algorithm);
|
||||
}
|
||||
|
||||
public @Nonnull Iterator<CompressedData> getCompressionLayers() {
|
||||
return new LayerIterator<CompressedData>(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<CompressionAlgorithm> 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 <pre>null</pre>.
|
||||
*
|
||||
* @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<SessionKey> getSessionKeys() {
|
||||
return map(getEncryptionLayers(), encryptedData -> encryptedData.sessionKey);
|
||||
}
|
||||
|
||||
public boolean isVerifiedSignedBy(@Nonnull PGPKeyRing keys) {
|
||||
return isVerifiedInlineSignedBy(keys) || isVerifiedDetachedSignedBy(keys);
|
||||
}
|
||||
|
||||
public List<SignatureVerification> getVerifiedSignatures() {
|
||||
List<SignatureVerification> 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<SignatureVerification> getVerifiedDetachedSignatures() {
|
||||
return message.getVerifiedDetachedSignatures();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of all rejected detached signatures.
|
||||
*
|
||||
* @return rejected detached signatures
|
||||
*/
|
||||
public @Nonnull List<SignatureVerification.Failure> 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<SignatureVerification> getVerifiedInlineSignatures() {
|
||||
List<SignatureVerification> verifications = new ArrayList<>();
|
||||
Iterator<List<SignatureVerification>> 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<List<SignatureVerification>> getVerifiedInlineSignaturesByLayer() {
|
||||
return new LayerIterator<List<SignatureVerification>>(message) {
|
||||
@Override
|
||||
boolean matches(Packet layer) {
|
||||
return layer instanceof Layer;
|
||||
}
|
||||
|
||||
@Override
|
||||
List<SignatureVerification> getProperty(Layer last) {
|
||||
List<SignatureVerification> 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<SignatureVerification.Failure> getRejectedInlineSignatures() {
|
||||
List<SignatureVerification.Failure> rejected = new ArrayList<>();
|
||||
Iterator<List<SignatureVerification.Failure>> 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<List<SignatureVerification.Failure>> getRejectedInlineSignaturesByLayer() {
|
||||
return new LayerIterator<List<SignatureVerification.Failure>>(message) {
|
||||
@Override
|
||||
boolean matches(Packet layer) {
|
||||
return layer instanceof Layer;
|
||||
}
|
||||
|
||||
@Override
|
||||
List<SignatureVerification.Failure> getProperty(Layer last) {
|
||||
List<SignatureVerification.Failure> list = new ArrayList<>();
|
||||
list.addAll(last.getRejectedOnePassSignatures());
|
||||
list.addAll(last.getRejectedPrependedSignatures());
|
||||
return list;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static boolean containsSignatureBy(@Nonnull List<SignatureVerification> 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 <a href="https://www.rfc-editor.org/rfc/rfc4880#section-5.9">RFC4880 §5.9. Literal Data Packet</a>
|
||||
*/
|
||||
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 <a href="https://www.rfc-editor.org/rfc/rfc4880#section-5.9">RFC4880 §5.9. Literal Data Packet</a>
|
||||
*/
|
||||
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 <a href="https://www.rfc-editor.org/rfc/rfc4880#section-5.9">RFC4880 §5.9. Literal Data Packet</a>
|
||||
*/
|
||||
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 <pre>null</pre>.
|
||||
*
|
||||
* @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 <pre>null</pre>.
|
||||
*
|
||||
* @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<SignatureVerification> verifiedDetachedSignatures = new ArrayList<>();
|
||||
protected final List<SignatureVerification.Failure> rejectedDetachedSignatures = new ArrayList<>();
|
||||
protected final List<SignatureVerification> verifiedOnePassSignatures = new ArrayList<>();
|
||||
protected final List<SignatureVerification.Failure> rejectedOnePassSignatures = new ArrayList<>();
|
||||
protected final List<SignatureVerification> verifiedPrependedSignatures = new ArrayList<>();
|
||||
protected final List<SignatureVerification.Failure> 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 <pre>null</pre>, 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<SignatureVerification> 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<SignatureVerification.Failure> 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<SignatureVerification> 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<SignatureVerification.Failure> 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<SignatureVerification> 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<SignatureVerification.Failure> 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 <pre>true</pre> if message is cleartext-signed, <pre>false</pre> otherwise
|
||||
* @see <a href="https://www.rfc-editor.org/rfc/rfc4880#section-7">RFC4880 §7. Cleartext Signature Framework</a>
|
||||
*/
|
||||
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 <pre>""</pre> indicates no filename.
|
||||
*
|
||||
* @return filename
|
||||
*/
|
||||
public @Nonnull String getFileName() {
|
||||
return fileName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the value of the modification date field.
|
||||
* A special date <pre>{@code new Date(0L)}</pre> 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<Long> 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<Long> 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<O> implements Iterator<O> {
|
||||
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 <A,B> Iterator<B> map(Iterator<A> from, Function<A, B> mapping) {
|
||||
return new Iterator<B>() {
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
return from.hasNext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public B next() {
|
||||
return mapping.apply(from.next());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public interface Function<A, B> {
|
||||
B apply(A item);
|
||||
}
|
||||
|
||||
private static @Nullable <A> A firstOrNull(Iterator<A> iterator) {
|
||||
if (iterator.hasNext()) {
|
||||
return iterator.next();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static @Nonnull <A> A firstOr(Iterator<A> iterator, A item) {
|
||||
if (iterator.hasNext()) {
|
||||
return iterator.next();
|
||||
}
|
||||
return item;
|
||||
}
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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 <a href="https://datatracker.ietf.org/doc/html/rfc4880#section-5.4">RFC</a>
|
||||
*/
|
||||
@Nullable PGPPublicKeyRing onMissingPublicKeyEncountered(@Nonnull Long keyId);
|
||||
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -1,380 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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<Long> recipientKeyIds;
|
||||
private final SubkeyIdentifier decryptionKey;
|
||||
private final List<SignatureVerification> verifiedInbandSignatures;
|
||||
private final List<SignatureVerification.Failure> invalidInbandSignatures;
|
||||
private final List<SignatureVerification> verifiedDetachedSignatures;
|
||||
private final List<SignatureVerification.Failure> 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<Long> recipientKeyIds,
|
||||
SubkeyIdentifier decryptionKey,
|
||||
SessionKey sessionKey,
|
||||
CompressionAlgorithm algorithm,
|
||||
List<SignatureVerification> verifiedInbandSignatures,
|
||||
List<SignatureVerification.Failure> invalidInbandSignatures,
|
||||
List<SignatureVerification> verifiedDetachedSignatures,
|
||||
List<SignatureVerification.Failure> 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<Long> 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<PGPSignature> getSignatures() {
|
||||
Set<PGPSignature> 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<SubkeyIdentifier, PGPSignature> getVerifiedSignatures() {
|
||||
Map<SubkeyIdentifier, PGPSignature> 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<SignatureVerification> getVerifiedInbandSignatures() {
|
||||
return verifiedInbandSignatures;
|
||||
}
|
||||
|
||||
public List<SignatureVerification> getVerifiedDetachedSignatures() {
|
||||
return verifiedDetachedSignatures;
|
||||
}
|
||||
|
||||
public List<SignatureVerification.Failure> getInvalidInbandSignatures() {
|
||||
return invalidInbandSignatures;
|
||||
}
|
||||
|
||||
public List<SignatureVerification.Failure> 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<Long> 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<SignatureVerification> verifiedInbandSignatures = new ArrayList<>();
|
||||
private final List<SignatureVerification> verifiedDetachedSignatures = new ArrayList<>();
|
||||
private final List<SignatureVerification.Failure> invalidInbandSignatures = new ArrayList<>();
|
||||
private final List<SignatureVerification.Failure> 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,106 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,159 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,169 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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 == ' ';
|
||||
}
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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();
|
||||
}
|
||||
}
|
|
@ -1,70 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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();
|
||||
}
|
||||
}
|
|
@ -1,54 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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);
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
/**
|
||||
* Classes related to cleartext signature verification.
|
||||
*/
|
||||
package org.pgpainless.decryption_verification.cleartext_signatures;
|
|
@ -1,45 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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
|
||||
}
|
|
@ -1,142 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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 <a href="https://www.rfc-editor.org/rfc/rfc4880#section-11.3">
|
||||
* rfc4880 - §11.3. OpenPGP Messages</a>
|
||||
* @see <a href="https://blog.jabberhead.tk/2022/09/14/using-pushdown-automata-to-verify-packet-sequences/">
|
||||
* Blog post about theoretic background and translation of grammar to PDA syntax</a>
|
||||
* @see <a href="https://blog.jabberhead.tk/2022/10/26/implementing-packet-sequence-validation-using-pushdown-automata/">
|
||||
* Blog post about practically implementing the PDA for packet syntax validation</a>
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
|
@ -1,156 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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 <a href="https://www.rfc-editor.org/rfc/rfc4880#section-11.3">OpenPGP Message Syntax</a>
|
||||
*/
|
||||
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<StackSymbol> stack = new Stack<>();
|
||||
private final List<InputSymbol> 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;
|
||||
}
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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} <pre>from</pre> for {@link InputSymbol} <pre>input</pre>
|
||||
* with {@link StackSymbol} <pre>stackItem</pre> 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;
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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<StackSymbol> 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<StackSymbol> getPushedItems() {
|
||||
return new ArrayList<>(pushedItems);
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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;
|
|
@ -1,66 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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<PGPPublicKey> 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();
|
||||
}
|
||||
}
|
|
@ -1,174 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,55 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021 David Hook <dgh@cryptoworkshop.com>
|
||||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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();
|
||||
}
|
||||
}
|
|
@ -1,77 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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<Set<SymmetricKeyAlgorithm>> 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();
|
||||
}
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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;
|
||||
|
||||
}
|
||||
}
|
|
@ -1,473 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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.
|
||||
* <p>
|
||||
* A typical use might look like follows:
|
||||
* <pre>
|
||||
* {@code
|
||||
* EncryptionOptions opt = new EncryptionOptions();
|
||||
* opt.addRecipient(aliceKey, "Alice <alice@wonderland.lit>");
|
||||
* opt.addPassphrase(Passphrase.fromPassword("AdditionalDecryptionPassphrase123"));
|
||||
* }
|
||||
* </pre>
|
||||
*<p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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
|
||||
* <pre>
|
||||
* {@code
|
||||
* opt.addRecipient(aliceKey, EncryptionOptions.encryptToFirstSubkey());
|
||||
* }
|
||||
* </pre>
|
||||
* when adding the recipient key.
|
||||
*/
|
||||
public class EncryptionOptions {
|
||||
|
||||
private final EncryptionPurpose purpose;
|
||||
private final Set<PGPKeyEncryptionMethodGenerator> encryptionMethods = new LinkedHashSet<>();
|
||||
private final Set<SubkeyIdentifier> encryptionKeys = new LinkedHashSet<>();
|
||||
private final Map<SubkeyIdentifier, KeyRingInfo> keyRingInfo = new HashMap<>();
|
||||
private final Map<SubkeyIdentifier, KeyAccessor> 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.
|
||||
* <p>
|
||||
* 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<CertificateAuthenticity> 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<PGPPublicKeyRing> 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<PGPPublicKeyRing> 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<PGPPublicKey> 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<PGPPublicKey> 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<PGPPublicKey> 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<PGPKeyEncryptionMethodGenerator> getEncryptionMethods() {
|
||||
return new HashSet<>(encryptionMethods);
|
||||
}
|
||||
|
||||
Map<SubkeyIdentifier, KeyRingInfo> getKeyRingInfo() {
|
||||
return new HashMap<>(keyRingInfo);
|
||||
}
|
||||
|
||||
Set<SubkeyIdentifier> getEncryptionKeyIdentifiers() {
|
||||
return new HashSet<>(encryptionKeys);
|
||||
}
|
||||
|
||||
Map<SubkeyIdentifier, KeyAccessor> 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 <pre>true</pre> iff the user specified at least one encryption method,
|
||||
* <pre>false</pre> otherwise.
|
||||
*
|
||||
* @return encryption methods is not empty
|
||||
*/
|
||||
public boolean hasEncryptionMethod() {
|
||||
return !encryptionMethods.isEmpty();
|
||||
}
|
||||
|
||||
public interface EncryptionKeySelector {
|
||||
List<PGPPublicKey> selectEncryptionSubkeys(@Nonnull List<PGPPublicKey> 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<PGPPublicKey> selectEncryptionSubkeys(@Nonnull List<PGPPublicKey> 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<PGPPublicKey> selectEncryptionSubkeys(@Nonnull List<PGPPublicKey> encryptionCapableKeys) {
|
||||
return encryptionCapableKeys;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: Create encryptToBestSubkey() method
|
||||
}
|
|
@ -1,209 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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<SubkeyIdentifier, PGPSignature> detachedSignatures;
|
||||
private final Set<SubkeyIdentifier> recipients;
|
||||
private final String fileName;
|
||||
private final Date modificationDate;
|
||||
private final StreamEncoding fileEncoding;
|
||||
|
||||
private EncryptionResult(SymmetricKeyAlgorithm encryptionAlgorithm,
|
||||
CompressionAlgorithm compressionAlgorithm,
|
||||
MultiMap<SubkeyIdentifier, PGPSignature> detachedSignatures,
|
||||
Set<SubkeyIdentifier> 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<SubkeyIdentifier, PGPSignature> getDetachedSignatures() {
|
||||
return detachedSignatures;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the set of recipient encryption keys.
|
||||
*
|
||||
* @return recipients
|
||||
*/
|
||||
public Set<SubkeyIdentifier> 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<SubkeyIdentifier, PGPSignature> detachedSignatures = new MultiMap<>();
|
||||
private final Set<SubkeyIdentifier> 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,313 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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 <a href="https://github.com/neuhalje/bouncy-gpg/blob/master/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/encrypting/PGPEncryptingStream.java">Source</a>
|
||||
*/
|
||||
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<HashAlgorithm> hashAlgorithms = new HashSet<>();
|
||||
if (signOpts != null) {
|
||||
for (SigningOptions.SigningMethod method : signOpts.getSigningMethods().values()) {
|
||||
hashAlgorithms.add(method.getHashAlgorithm());
|
||||
}
|
||||
}
|
||||
|
||||
int[] algorithmIds = new int[hashAlgorithms.size()];
|
||||
Iterator<HashAlgorithm> 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<SubkeyIdentifier> 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;
|
||||
}
|
||||
}
|
|
@ -1,83 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,355 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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'.
|
||||
* <br>
|
||||
* 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.
|
||||
* <br>
|
||||
* 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}.
|
||||
* <br>
|
||||
* 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 <a href="https://datatracker.ietf.org/doc/html/rfc4880#section-5.9">RFC4880 §5.9. Literal Data Packet</a>
|
||||
*
|
||||
* @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.
|
||||
* <br>
|
||||
* 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 <pre>true</pre>, 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 <pre>true</pre>.
|
||||
*
|
||||
* @param hideArmorHeaders true or false
|
||||
* @return this
|
||||
*/
|
||||
public ProducerOptions setHideArmorHeaders(boolean hideArmorHeaders) {
|
||||
this.hideArmorHeaders = hideArmorHeaders;
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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();
|
||||
}
|
||||
}
|
|
@ -1,634 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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<SubkeyIdentifier, SigningMethod> 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<PGPSecretKeyRing> 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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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<PGPPublicKey> 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<HashAlgorithm> 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<PGPPublicKey> 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<HashAlgorithm> 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<PGPSecretKeyRing> 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).
|
||||
* <p>
|
||||
* 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).
|
||||
* <p>
|
||||
* 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<PGPPublicKey> 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<HashAlgorithm> 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<PGPPublicKey> 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<HashAlgorithm> 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.
|
||||
* <p>
|
||||
* 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<HashAlgorithm> 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<SubkeyIdentifier, SigningMethod> 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.
|
||||
* <p>
|
||||
* 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;
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
/**
|
||||
* Classes used to encrypt or sign data using OpenPGP.
|
||||
*/
|
||||
package org.pgpainless.encryption_signing;
|
|
@ -1,154 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2020 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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);
|
||||
}
|
||||
}
|
|
@ -1,119 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2020 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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();
|
||||
}
|
||||
}
|
|
@ -1,141 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2020 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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());
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2020 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
/**
|
||||
* Implementation factory classes to be able to switch out the underlying crypto engine implementation.
|
||||
*/
|
||||
package org.pgpainless.implementation;
|
|
@ -1,189 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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<OpenPgpFingerprint> {
|
||||
@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 <a href="https://tools.ietf.org/html/rfc4880#section-12.2">
|
||||
* RFC-4880 §12.2: Key IDs and Fingerprints</a>
|
||||
* @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();
|
||||
}
|
|
@ -1,149 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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 <a href="https://metacode.biz/openpgp/openpgp4fpr">openpgp4fpr URI scheme</a>
|
||||
*/
|
||||
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 <a href="https://metacode.biz/openpgp/openpgp4fpr">openpgp4fpr URI scheme</a>
|
||||
*/
|
||||
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());
|
||||
}
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,146 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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();
|
||||
}
|
||||
}
|
|
@ -1,119 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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());
|
||||
}
|
||||
}
|
|
@ -1,298 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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);
|
||||
}
|
||||
|
||||
if (!info.isUsableForThirdPartyCertification()) {
|
||||
throw new KeyException.UnacceptableThirdPartyCertificationKeyException(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;
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
/**
|
||||
* API for key certifications.
|
||||
*/
|
||||
package org.pgpainless.key.certification;
|
|
@ -1,114 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>, 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<PGPSecretKeyRing> secretKeyRings = new ArrayList<>();
|
||||
List<PGPPublicKeyRing> 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<PGPKeyRing> collection, boolean isSilent)
|
||||
throws PGPException {
|
||||
List<PGPSecretKeyRing> secretKeyRings = new ArrayList<>();
|
||||
List<PGPPublicKeyRing> 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();
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
/**
|
||||
* OpenPGP key collections.
|
||||
*/
|
||||
package org.pgpainless.key.collection;
|
|
@ -1,313 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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.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<KeyRingBuilder> {
|
||||
|
||||
private static final long YEAR_IN_SECONDS = 1000L * 60 * 60 * 24 * 365;
|
||||
|
||||
private KeySpec primaryKeySpec;
|
||||
private final List<KeySpec> subkeySpecs = new ArrayList<>();
|
||||
private final Map<String, SelfSignatureSubpackets.Callback> 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 (!keyIsCertificationCapable(spec)) {
|
||||
throw new IllegalArgumentException("Key algorithm " + spec.getKeyType().getName() + " is not capable of creating certifications.");
|
||||
}
|
||||
}
|
||||
|
||||
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<PGPSecretKey> 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<Map.Entry<String, SelfSignatureSubpackets.Callback>> userIdIterator =
|
||||
this.userIds.entrySet().iterator();
|
||||
if (userIdIterator.hasNext()) {
|
||||
userIdIterator.next(); // Skip primary user id
|
||||
}
|
||||
while (userIdIterator.hasNext()) {
|
||||
Map.Entry<String, SelfSignatureSubpackets.Callback> 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<PGPSecretKey> secretKeyList = new ArrayList<>();
|
||||
secretKeyList.add(primarySecKey);
|
||||
while (secretKeys.hasNext()) {
|
||||
secretKeyList.add(secretKeys.next());
|
||||
}
|
||||
secretKeyRing = new PGPSecretKeyRing(secretKeyList);
|
||||
return secretKeyRing;
|
||||
}
|
||||
|
||||
private void addSubKeys(PGPKeyPair primaryKey, PGPKeyRingGenerator ringGenerator)
|
||||
throws NoSuchAlgorithmException, PGPException, InvalidAlgorithmParameterException {
|
||||
for (KeySpec subKeySpec : subkeySpecs) {
|
||||
PGPKeyPair subKey = generateKeyPair(subKeySpec);
|
||||
if (subKeySpec.isInheritedSubPackets()) {
|
||||
ringGenerator.addSubKey(subKey);
|
||||
} else {
|
||||
PGPSignatureSubpacketVector hashedSubpackets = subKeySpec.getSubpackets();
|
||||
try {
|
||||
hashedSubpackets = addPrimaryKeyBindingSignatureIfNecessary(
|
||||
primaryKey, subKey, hashedSubpackets);
|
||||
} catch (IOException e) {
|
||||
throw new PGPException("Exception while adding primary key binding signature to signing subkey", e);
|
||||
}
|
||||
ringGenerator.addSubKey(subKey, hashedSubpackets, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private PGPSignatureSubpacketVector addPrimaryKeyBindingSignatureIfNecessary(
|
||||
PGPKeyPair primaryKey, PGPKeyPair subKey, PGPSignatureSubpacketVector hashedSubpackets)
|
||||
throws PGPException, IOException {
|
||||
int keyFlagMask = hashedSubpackets.getKeyFlags();
|
||||
if (!KeyFlag.hasKeyFlag(keyFlagMask, KeyFlag.SIGN_DATA) &&
|
||||
!KeyFlag.hasKeyFlag(keyFlagMask, KeyFlag.CERTIFY_OTHER)) {
|
||||
return hashedSubpackets;
|
||||
}
|
||||
|
||||
PGPSignatureGenerator bindingSignatureGenerator = new PGPSignatureGenerator(buildContentSigner(subKey));
|
||||
bindingSignatureGenerator.init(SignatureType.PRIMARYKEY_BINDING.getCode(), subKey.getPrivateKey());
|
||||
PGPSignature primaryKeyBindingSig = bindingSignatureGenerator.generateCertification(primaryKey.getPublicKey(), subKey.getPublicKey());
|
||||
PGPSignatureSubpacketGenerator subpacketGenerator = new PGPSignatureSubpacketGenerator(hashedSubpackets);
|
||||
subpacketGenerator.addEmbeddedSignature(false, primaryKeyBindingSig);
|
||||
return subpacketGenerator.generate();
|
||||
}
|
||||
|
||||
private PGPContentSignerBuilder buildContentSigner(PGPKeyPair certKey) {
|
||||
HashAlgorithm hashAlgorithm = PGPainless.getPolicy()
|
||||
.getSignatureHashAlgorithmPolicy().defaultHashAlgorithm();
|
||||
return ImplementationFactory.getInstance().getPGPContentSignerBuilder(
|
||||
certKey.getPublicKey().getAlgorithm(),
|
||||
hashAlgorithm.getAlgorithmId());
|
||||
}
|
||||
|
||||
private PBESecretKeyEncryptor buildSecretKeyEncryptor(PGPDigestCalculator keyFingerprintCalculator) {
|
||||
SymmetricKeyAlgorithm keyEncryptionAlgorithm = PGPainless.getPolicy()
|
||||
.getSymmetricKeyEncryptionAlgorithmPolicy()
|
||||
.getDefaultSymmetricKeyAlgorithm();
|
||||
if (!passphrase.isValid()) {
|
||||
throw new IllegalStateException("Passphrase was cleared.");
|
||||
}
|
||||
return passphrase.isEmpty() ? null : // unencrypted key pair, otherwise AES-256 encrypted
|
||||
ImplementationFactory.getInstance().getPBESecretKeyEncryptor(
|
||||
keyEncryptionAlgorithm, keyFingerprintCalculator, passphrase);
|
||||
}
|
||||
|
||||
private PBESecretKeyDecryptor buildSecretKeyDecryptor() throws PGPException {
|
||||
if (!passphrase.isValid()) {
|
||||
throw new IllegalStateException("Passphrase was cleared.");
|
||||
}
|
||||
return passphrase.isEmpty() ? null :
|
||||
ImplementationFactory.getInstance().getPBESecretKeyDecryptor(passphrase);
|
||||
}
|
||||
|
||||
public static PGPKeyPair generateKeyPair(KeySpec spec)
|
||||
throws NoSuchAlgorithmException, PGPException,
|
||||
InvalidAlgorithmParameterException {
|
||||
KeyType type = spec.getKeyType();
|
||||
KeyPairGenerator certKeyGenerator = KeyPairGenerator.getInstance(type.getName(),
|
||||
ProviderFactory.getProvider());
|
||||
certKeyGenerator.initialize(type.getAlgorithmSpec());
|
||||
|
||||
// Create raw Key Pair
|
||||
KeyPair keyPair = certKeyGenerator.generateKeyPair();
|
||||
|
||||
Date keyCreationDate = spec.getKeyCreationDate() != null ? spec.getKeyCreationDate() : new Date();
|
||||
|
||||
// Form PGP key pair
|
||||
PGPKeyPair pgpKeyPair = ImplementationFactory.getInstance()
|
||||
.getPGPKeyPair(type.getAlgorithm(), keyPair, keyCreationDate);
|
||||
return pgpKeyPair;
|
||||
}
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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 extends KeyRingBuilderInterface<B>> {
|
||||
|
||||
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;
|
||||
}
|
|
@ -1,255 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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();
|
||||
}
|
||||
|
||||
}
|
|
@ -1,62 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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... flags) {
|
||||
return new KeySpecBuilder(type, flags);
|
||||
}
|
||||
}
|
|
@ -1,88 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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;
|
||||
|
||||
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<CompressionAlgorithm> preferredCompressionAlgorithms = algorithmSuite.getCompressionAlgorithms();
|
||||
private Set<HashAlgorithm> preferredHashAlgorithms = algorithmSuite.getHashAlgorithms();
|
||||
private Set<SymmetricKeyAlgorithm> preferredSymmetricAlgorithms = algorithmSuite.getSymmetricKeyAlgorithms();
|
||||
private Date keyCreationDate;
|
||||
|
||||
KeySpecBuilder(@Nonnull KeyType type, KeyFlag... flags) {
|
||||
if (flags == null) {
|
||||
this.keyFlags = new KeyFlag[0];
|
||||
} else {
|
||||
SignatureSubpacketsUtil.assureKeyCanCarryFlags(type, flags);
|
||||
this.keyFlags = flags;
|
||||
}
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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();
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
/**
|
||||
* Classes related to OpenPGP key generation.
|
||||
*/
|
||||
package org.pgpainless.key.generation;
|
|
@ -1,10 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package org.pgpainless.key.generation.type;
|
||||
|
||||
public interface KeyLength {
|
||||
|
||||
int getLength();
|
||||
}
|
|
@ -1,118 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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);
|
||||
}
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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;
|
||||
}
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2020 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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());
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2020 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
/**
|
||||
* Classes related to ECDH.
|
||||
*/
|
||||
package org.pgpainless.key.generation.type.ecc.ecdh;
|
|
@ -1,48 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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());
|
||||
}
|
||||
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2020 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
/**
|
||||
* Classes related to ECDSA.
|
||||
*/
|
||||
package org.pgpainless.key.generation.type.ecc.ecdsa;
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue