1
0
Fork 0
mirror of https://github.com/pgpainless/pgpainless.git synced 2025-09-11 11:19:39 +02:00

Compare commits

..

No commits in common. "main" and "1.3.11" have entirely different histories.
main ... 1.3.11

607 changed files with 26926 additions and 32867 deletions

View file

@ -1,51 +1,513 @@
# This .editorconfig section approximates ktfmt's formatting rules. You can include it in an
# existing .editorconfig file or use it standalone by copying it to <project root>/.editorconfig
# and making sure your editor is set to read settings from .editorconfig files.
# SPDX-FileCopyrightText: 2021 Paul Schaub <info@pgpainless.org>
#
# It includes editor-specific config options for IntelliJ IDEA.
#
# If any option is wrong, PR are welcome
# SPDX-License-Identifier: CC0-1.0
[{*.kt,*.kts}]
indent_style = space
insert_final_newline = true
max_line_length = 100
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
ij_continuation_indent_size = 4 # was 8
ij_java_names_count_to_use_import_on_demand = 9999
indent_style = space
insert_final_newline = false
max_line_length = 120
tab_width = 4
ij_continuation_indent_size = 8
ij_formatter_off_tag = @formatter:off
ij_formatter_on_tag = @formatter:on
ij_formatter_tags_enabled = false
ij_smart_tabs = false
ij_visual_guides = none
ij_wrap_on_typing = false
[*.java]
ij_java_align_consecutive_assignments = false
ij_java_align_consecutive_variable_declarations = false
ij_java_align_group_field_declarations = false
ij_java_align_multiline_annotation_parameters = false
ij_java_align_multiline_array_initializer_expression = false
ij_java_align_multiline_assignment = false
ij_java_align_multiline_binary_operation = false
ij_java_align_multiline_chained_methods = false
ij_java_align_multiline_extends_list = false
ij_java_align_multiline_for = true
ij_java_align_multiline_method_parentheses = false
ij_java_align_multiline_parameters = true
ij_java_align_multiline_parameters_in_calls = false
ij_java_align_multiline_parenthesized_expression = false
ij_java_align_multiline_records = true
ij_java_align_multiline_resources = true
ij_java_align_multiline_ternary_operation = false
ij_java_align_multiline_text_blocks = false
ij_java_align_multiline_throws_list = false
ij_java_align_subsequent_simple_methods = false
ij_java_align_throws_keyword = false
ij_java_annotation_parameter_wrap = off
ij_java_array_initializer_new_line_after_left_brace = false
ij_java_array_initializer_right_brace_on_new_line = false
ij_java_array_initializer_wrap = off
ij_java_assert_statement_colon_on_next_line = false
ij_java_assert_statement_wrap = off
ij_java_assignment_wrap = off
ij_java_binary_operation_sign_on_next_line = false
ij_java_binary_operation_wrap = off
ij_java_blank_lines_after_anonymous_class_header = 0
ij_java_blank_lines_after_class_header = 0
ij_java_blank_lines_after_imports = 1
ij_java_blank_lines_after_package = 1
ij_java_blank_lines_around_class = 1
ij_java_blank_lines_around_field = 0
ij_java_blank_lines_around_field_in_interface = 0
ij_java_blank_lines_around_initializer = 1
ij_java_blank_lines_around_method = 1
ij_java_blank_lines_around_method_in_interface = 1
ij_java_blank_lines_before_class_end = 0
ij_java_blank_lines_before_imports = 1
ij_java_blank_lines_before_method_body = 0
ij_java_blank_lines_before_package = 0
ij_java_block_brace_style = end_of_line
ij_java_block_comment_at_first_column = true
ij_java_builder_methods = none
ij_java_call_parameters_new_line_after_left_paren = false
ij_java_call_parameters_right_paren_on_new_line = false
ij_java_call_parameters_wrap = off
ij_java_case_statement_on_separate_line = true
ij_java_catch_on_new_line = false
ij_java_class_annotation_wrap = split_into_lines
ij_java_class_brace_style = end_of_line
ij_java_class_count_to_use_import_on_demand = 10000
ij_java_class_names_in_javadoc = 1
ij_java_do_not_indent_top_level_class_members = false
ij_java_do_not_wrap_after_single_annotation = false
ij_java_do_while_brace_force = never
ij_java_doc_add_blank_line_after_description = true
ij_java_doc_add_blank_line_after_param_comments = false
ij_java_doc_add_blank_line_after_return = false
ij_java_doc_add_p_tag_on_empty_lines = true
ij_java_doc_align_exception_comments = true
ij_java_doc_align_param_comments = true
ij_java_doc_do_not_wrap_if_one_line = false
ij_java_doc_enable_formatting = true
ij_java_doc_enable_leading_asterisks = true
ij_java_doc_indent_on_continuation = false
ij_java_doc_keep_empty_lines = true
ij_java_doc_keep_empty_parameter_tag = true
ij_java_doc_keep_empty_return_tag = true
ij_java_doc_keep_empty_throws_tag = true
ij_java_doc_keep_invalid_tags = true
ij_java_doc_param_description_on_new_line = false
ij_java_doc_preserve_line_breaks = false
ij_java_doc_use_throws_not_exception_tag = true
ij_java_else_on_new_line = false
ij_java_enum_constants_wrap = off
ij_java_extends_keyword_wrap = off
ij_java_extends_list_wrap = off
ij_java_field_annotation_wrap = split_into_lines
ij_java_finally_on_new_line = false
ij_java_for_brace_force = never
ij_java_for_statement_new_line_after_left_paren = false
ij_java_for_statement_right_paren_on_new_line = false
ij_java_for_statement_wrap = off
ij_java_generate_final_locals = false
ij_java_generate_final_parameters = false
ij_java_if_brace_force = never
ij_java_imports_layout = $*,|,java.**,javax.**,|,*
ij_java_indent_case_from_switch = true
ij_java_insert_inner_class_imports = false
ij_java_insert_override_annotation = true
ij_java_keep_blank_lines_before_right_brace = 2
ij_java_keep_blank_lines_between_package_declaration_and_header = 2
ij_java_keep_blank_lines_in_code = 2
ij_java_keep_blank_lines_in_declarations = 2
ij_java_keep_builder_methods_indents = false
ij_java_keep_control_statement_in_one_line = true
ij_java_keep_first_column_comment = true
ij_java_keep_indents_on_empty_lines = false
ij_java_keep_line_breaks = true
ij_java_keep_multiple_expressions_in_one_line = false
ij_java_keep_simple_blocks_in_one_line = false
ij_java_keep_simple_classes_in_one_line = false
ij_java_keep_simple_lambdas_in_one_line = false
ij_java_keep_simple_methods_in_one_line = false
ij_java_label_indent_absolute = false
ij_java_label_indent_size = 0
ij_java_lambda_brace_style = end_of_line
ij_java_layout_static_imports_separately = true
ij_java_line_comment_add_space = false
ij_java_line_comment_at_first_column = true
ij_java_method_annotation_wrap = split_into_lines
ij_java_method_brace_style = end_of_line
ij_java_method_call_chain_wrap = off
ij_java_method_parameters_new_line_after_left_paren = false
ij_java_method_parameters_right_paren_on_new_line = false
ij_java_method_parameters_wrap = off
ij_java_modifier_list_wrap = false
ij_java_names_count_to_use_import_on_demand = 1000
ij_java_new_line_after_lparen_in_record_header = false
ij_java_packages_to_use_import_on_demand = java.awt.*,javax.swing.*
ij_java_parameter_annotation_wrap = off
ij_java_parentheses_expression_new_line_after_left_paren = false
ij_java_parentheses_expression_right_paren_on_new_line = false
ij_java_place_assignment_sign_on_next_line = false
ij_java_prefer_longer_names = true
ij_java_prefer_parameters_wrap = false
ij_java_record_components_wrap = normal
ij_java_repeat_synchronized = true
ij_java_replace_instanceof_and_cast = false
ij_java_replace_null_check = true
ij_java_replace_sum_lambda_with_method_ref = true
ij_java_resource_list_new_line_after_left_paren = false
ij_java_resource_list_right_paren_on_new_line = false
ij_java_resource_list_wrap = off
ij_java_rparen_on_new_line_in_record_header = false
ij_java_space_after_closing_angle_bracket_in_type_argument = false
ij_java_space_after_colon = true
ij_java_space_after_comma = true
ij_java_space_after_comma_in_type_arguments = true
ij_java_space_after_for_semicolon = true
ij_java_space_after_quest = true
ij_java_space_after_type_cast = true
ij_java_space_before_annotation_array_initializer_left_brace = false
ij_java_space_before_annotation_parameter_list = false
ij_java_space_before_array_initializer_left_brace = false
ij_java_space_before_catch_keyword = true
ij_java_space_before_catch_left_brace = true
ij_java_space_before_catch_parentheses = true
ij_java_space_before_class_left_brace = true
ij_java_space_before_colon = true
ij_java_space_before_colon_in_foreach = true
ij_java_space_before_comma = false
ij_java_space_before_do_left_brace = true
ij_java_space_before_else_keyword = true
ij_java_space_before_else_left_brace = true
ij_java_space_before_finally_keyword = true
ij_java_space_before_finally_left_brace = true
ij_java_space_before_for_left_brace = true
ij_java_space_before_for_parentheses = true
ij_java_space_before_for_semicolon = false
ij_java_space_before_if_left_brace = true
ij_java_space_before_if_parentheses = true
ij_java_space_before_method_call_parentheses = false
ij_java_space_before_method_left_brace = true
ij_java_space_before_method_parentheses = false
ij_java_space_before_opening_angle_bracket_in_type_parameter = false
ij_java_space_before_quest = true
ij_java_space_before_switch_left_brace = true
ij_java_space_before_switch_parentheses = true
ij_java_space_before_synchronized_left_brace = true
ij_java_space_before_synchronized_parentheses = true
ij_java_space_before_try_left_brace = true
ij_java_space_before_try_parentheses = true
ij_java_space_before_type_parameter_list = false
ij_java_space_before_while_keyword = true
ij_java_space_before_while_left_brace = true
ij_java_space_before_while_parentheses = true
ij_java_space_inside_one_line_enum_braces = false
ij_java_space_within_empty_array_initializer_braces = false
ij_java_space_within_empty_method_call_parentheses = false
ij_java_space_within_empty_method_parentheses = false
ij_java_spaces_around_additive_operators = true
ij_java_spaces_around_assignment_operators = true
ij_java_spaces_around_bitwise_operators = true
ij_java_spaces_around_equality_operators = true
ij_java_spaces_around_lambda_arrow = true
ij_java_spaces_around_logical_operators = true
ij_java_spaces_around_method_ref_dbl_colon = false
ij_java_spaces_around_multiplicative_operators = true
ij_java_spaces_around_relational_operators = true
ij_java_spaces_around_shift_operators = true
ij_java_spaces_around_type_bounds_in_type_parameters = true
ij_java_spaces_around_unary_operator = false
ij_java_spaces_within_angle_brackets = false
ij_java_spaces_within_annotation_parentheses = false
ij_java_spaces_within_array_initializer_braces = false
ij_java_spaces_within_braces = false
ij_java_spaces_within_brackets = false
ij_java_spaces_within_cast_parentheses = false
ij_java_spaces_within_catch_parentheses = false
ij_java_spaces_within_for_parentheses = false
ij_java_spaces_within_if_parentheses = false
ij_java_spaces_within_method_call_parentheses = false
ij_java_spaces_within_method_parentheses = false
ij_java_spaces_within_parentheses = false
ij_java_spaces_within_record_header = false
ij_java_spaces_within_switch_parentheses = false
ij_java_spaces_within_synchronized_parentheses = false
ij_java_spaces_within_try_parentheses = false
ij_java_spaces_within_while_parentheses = false
ij_java_special_else_if_treatment = true
ij_java_subclass_name_suffix = Impl
ij_java_ternary_operation_signs_on_next_line = false
ij_java_ternary_operation_wrap = off
ij_java_test_name_suffix = Test
ij_java_throws_keyword_wrap = off
ij_java_throws_list_wrap = off
ij_java_use_external_annotations = false
ij_java_use_fq_class_names = false
ij_java_use_relative_indents = false
ij_java_use_single_class_imports = true
ij_java_variable_annotation_wrap = off
ij_java_visibility = public
ij_java_while_brace_force = never
ij_java_while_on_new_line = false
ij_java_wrap_comments = false
ij_java_wrap_first_method_in_call_chain = false
ij_java_wrap_long_lines = false
[*.properties]
ij_properties_align_group_field_declarations = false
ij_properties_keep_blank_lines = false
ij_properties_key_value_delimiter = equals
ij_properties_spaces_around_key_value_delimiter = false
[.editorconfig]
ij_editorconfig_align_group_field_declarations = false
ij_editorconfig_space_after_colon = false
ij_editorconfig_space_after_comma = true
ij_editorconfig_space_before_colon = false
ij_editorconfig_space_before_comma = false
ij_editorconfig_spaces_around_assignment_operators = true
[{*.ant,*.fxml,*.jhm,*.jnlp,*.jrxml,*.jspx,*.pom,*.rng,*.tagx,*.tld,*.wsdl,*.xml,*.xsd,*.xsl,*.xslt,*.xul}]
ij_xml_align_attributes = true
ij_xml_align_text = false
ij_xml_attribute_wrap = normal
ij_xml_block_comment_at_first_column = true
ij_xml_keep_blank_lines = 2
ij_xml_keep_indents_on_empty_lines = false
ij_xml_keep_line_breaks = true
ij_xml_keep_line_breaks_in_text = true
ij_xml_keep_whitespaces = false
ij_xml_keep_whitespaces_around_cdata = preserve
ij_xml_keep_whitespaces_inside_cdata = false
ij_xml_line_comment_at_first_column = true
ij_xml_space_after_tag_name = false
ij_xml_space_around_equals_in_attribute = false
ij_xml_space_inside_empty_tag = false
ij_xml_text_wrap = normal
ij_xml_use_custom_settings = false
[{*.bash,*.sh,*.zsh}]
indent_size = 2
tab_width = 2
ij_shell_binary_ops_start_line = false
ij_shell_keep_column_alignment_padding = false
ij_shell_minify_program = false
ij_shell_redirect_followed_by_space = false
ij_shell_switch_cases_indented = false
ij_shell_use_unix_line_separator = true
[{*.gant,*.gradle,*.groovy,*.gy}]
ij_groovy_align_group_field_declarations = false
ij_groovy_align_multiline_array_initializer_expression = false
ij_groovy_align_multiline_assignment = false
ij_groovy_align_multiline_binary_operation = false
ij_groovy_align_multiline_chained_methods = false
ij_groovy_align_multiline_extends_list = false
ij_groovy_align_multiline_for = true
ij_groovy_align_multiline_list_or_map = true
ij_groovy_align_multiline_method_parentheses = false
ij_groovy_align_multiline_parameters = true
ij_groovy_align_multiline_parameters_in_calls = false
ij_groovy_align_multiline_resources = true
ij_groovy_align_multiline_ternary_operation = false
ij_groovy_align_multiline_throws_list = false
ij_groovy_align_named_args_in_map = true
ij_groovy_align_throws_keyword = false
ij_groovy_array_initializer_new_line_after_left_brace = false
ij_groovy_array_initializer_right_brace_on_new_line = false
ij_groovy_array_initializer_wrap = off
ij_groovy_assert_statement_wrap = off
ij_groovy_assignment_wrap = off
ij_groovy_binary_operation_wrap = off
ij_groovy_blank_lines_after_class_header = 0
ij_groovy_blank_lines_after_imports = 1
ij_groovy_blank_lines_after_package = 1
ij_groovy_blank_lines_around_class = 1
ij_groovy_blank_lines_around_field = 0
ij_groovy_blank_lines_around_field_in_interface = 0
ij_groovy_blank_lines_around_method = 1
ij_groovy_blank_lines_around_method_in_interface = 1
ij_groovy_blank_lines_before_imports = 1
ij_groovy_blank_lines_before_method_body = 0
ij_groovy_blank_lines_before_package = 0
ij_groovy_block_brace_style = end_of_line
ij_groovy_block_comment_at_first_column = true
ij_groovy_call_parameters_new_line_after_left_paren = false
ij_groovy_call_parameters_right_paren_on_new_line = false
ij_groovy_call_parameters_wrap = off
ij_groovy_catch_on_new_line = false
ij_groovy_class_annotation_wrap = split_into_lines
ij_groovy_class_brace_style = end_of_line
ij_groovy_class_count_to_use_import_on_demand = 5
ij_groovy_do_while_brace_force = never
ij_groovy_else_on_new_line = false
ij_groovy_enum_constants_wrap = off
ij_groovy_extends_keyword_wrap = off
ij_groovy_extends_list_wrap = off
ij_groovy_field_annotation_wrap = split_into_lines
ij_groovy_finally_on_new_line = false
ij_groovy_for_brace_force = never
ij_groovy_for_statement_new_line_after_left_paren = false
ij_groovy_for_statement_right_paren_on_new_line = false
ij_groovy_for_statement_wrap = off
ij_groovy_if_brace_force = never
ij_groovy_import_annotation_wrap = 2
ij_groovy_imports_layout = *,|,javax.**,java.**,|,$*
ij_groovy_indent_case_from_switch = true
ij_groovy_indent_label_blocks = true
ij_groovy_insert_inner_class_imports = false
ij_groovy_keep_blank_lines_before_right_brace = 2
ij_groovy_keep_blank_lines_in_code = 2
ij_groovy_keep_blank_lines_in_declarations = 2
ij_groovy_keep_control_statement_in_one_line = true
ij_groovy_keep_first_column_comment = true
ij_groovy_keep_indents_on_empty_lines = false
ij_groovy_keep_line_breaks = true
ij_groovy_keep_multiple_expressions_in_one_line = false
ij_groovy_keep_simple_blocks_in_one_line = false
ij_groovy_keep_simple_classes_in_one_line = true
ij_groovy_keep_simple_lambdas_in_one_line = true
ij_groovy_keep_simple_methods_in_one_line = true
ij_groovy_label_indent_absolute = false
ij_groovy_label_indent_size = 0
ij_groovy_lambda_brace_style = end_of_line
ij_groovy_layout_static_imports_separately = true
ij_groovy_line_comment_add_space = false
ij_groovy_line_comment_at_first_column = true
ij_groovy_method_annotation_wrap = split_into_lines
ij_groovy_method_brace_style = end_of_line
ij_groovy_method_call_chain_wrap = off
ij_groovy_method_parameters_new_line_after_left_paren = false
ij_groovy_method_parameters_right_paren_on_new_line = false
ij_groovy_method_parameters_wrap = off
ij_groovy_modifier_list_wrap = false
ij_groovy_names_count_to_use_import_on_demand = 3
ij_groovy_parameter_annotation_wrap = off
ij_groovy_parentheses_expression_new_line_after_left_paren = false
ij_groovy_parentheses_expression_right_paren_on_new_line = false
ij_groovy_prefer_parameters_wrap = false
ij_groovy_resource_list_new_line_after_left_paren = false
ij_groovy_resource_list_right_paren_on_new_line = false
ij_groovy_resource_list_wrap = off
ij_groovy_space_after_assert_separator = true
ij_groovy_space_after_colon = true
ij_groovy_space_after_comma = true
ij_groovy_space_after_comma_in_type_arguments = true
ij_groovy_space_after_for_semicolon = true
ij_groovy_space_after_quest = true
ij_groovy_space_after_type_cast = true
ij_groovy_space_before_annotation_parameter_list = false
ij_groovy_space_before_array_initializer_left_brace = false
ij_groovy_space_before_assert_separator = false
ij_groovy_space_before_catch_keyword = true
ij_groovy_space_before_catch_left_brace = true
ij_groovy_space_before_catch_parentheses = true
ij_groovy_space_before_class_left_brace = true
ij_groovy_space_before_closure_left_brace = true
ij_groovy_space_before_colon = true
ij_groovy_space_before_comma = false
ij_groovy_space_before_do_left_brace = true
ij_groovy_space_before_else_keyword = true
ij_groovy_space_before_else_left_brace = true
ij_groovy_space_before_finally_keyword = true
ij_groovy_space_before_finally_left_brace = true
ij_groovy_space_before_for_left_brace = true
ij_groovy_space_before_for_parentheses = true
ij_groovy_space_before_for_semicolon = false
ij_groovy_space_before_if_left_brace = true
ij_groovy_space_before_if_parentheses = true
ij_groovy_space_before_method_call_parentheses = false
ij_groovy_space_before_method_left_brace = true
ij_groovy_space_before_method_parentheses = false
ij_groovy_space_before_quest = true
ij_groovy_space_before_switch_left_brace = true
ij_groovy_space_before_switch_parentheses = true
ij_groovy_space_before_synchronized_left_brace = true
ij_groovy_space_before_synchronized_parentheses = true
ij_groovy_space_before_try_left_brace = true
ij_groovy_space_before_try_parentheses = true
ij_groovy_space_before_while_keyword = true
ij_groovy_space_before_while_left_brace = true
ij_groovy_space_before_while_parentheses = true
ij_groovy_space_in_named_argument = true
ij_groovy_space_in_named_argument_before_colon = false
ij_groovy_space_within_empty_array_initializer_braces = false
ij_groovy_space_within_empty_method_call_parentheses = false
ij_groovy_spaces_around_additive_operators = true
ij_groovy_spaces_around_assignment_operators = true
ij_groovy_spaces_around_bitwise_operators = true
ij_groovy_spaces_around_equality_operators = true
ij_groovy_spaces_around_lambda_arrow = true
ij_groovy_spaces_around_logical_operators = true
ij_groovy_spaces_around_multiplicative_operators = true
ij_groovy_spaces_around_regex_operators = true
ij_groovy_spaces_around_relational_operators = true
ij_groovy_spaces_around_shift_operators = true
ij_groovy_spaces_within_annotation_parentheses = false
ij_groovy_spaces_within_array_initializer_braces = false
ij_groovy_spaces_within_braces = true
ij_groovy_spaces_within_brackets = false
ij_groovy_spaces_within_cast_parentheses = false
ij_groovy_spaces_within_catch_parentheses = false
ij_groovy_spaces_within_for_parentheses = false
ij_groovy_spaces_within_gstring_injection_braces = false
ij_groovy_spaces_within_if_parentheses = false
ij_groovy_spaces_within_list_or_map = false
ij_groovy_spaces_within_method_call_parentheses = false
ij_groovy_spaces_within_method_parentheses = false
ij_groovy_spaces_within_parentheses = false
ij_groovy_spaces_within_switch_parentheses = false
ij_groovy_spaces_within_synchronized_parentheses = false
ij_groovy_spaces_within_try_parentheses = false
ij_groovy_spaces_within_tuple_expression = false
ij_groovy_spaces_within_while_parentheses = false
ij_groovy_special_else_if_treatment = true
ij_groovy_ternary_operation_wrap = off
ij_groovy_throws_keyword_wrap = off
ij_groovy_throws_list_wrap = off
ij_groovy_use_flying_geese_braces = false
ij_groovy_use_fq_class_names = false
ij_groovy_use_fq_class_names_in_javadoc = true
ij_groovy_use_relative_indents = false
ij_groovy_use_single_class_imports = true
ij_groovy_variable_annotation_wrap = off
ij_groovy_while_brace_force = never
ij_groovy_while_on_new_line = false
ij_groovy_wrap_long_lines = false
[{*.gradle.kts,*.kt,*.kts,*.main.kts,*.space.kts}]
ij_kotlin_align_in_columns_case_branch = false
ij_kotlin_align_multiline_binary_operation = false
ij_kotlin_align_multiline_extends_list = false
ij_kotlin_align_multiline_method_parentheses = false
ij_kotlin_align_multiline_parameters = true
ij_kotlin_align_multiline_parameters_in_calls = false
ij_kotlin_allow_trailing_comma = true
ij_kotlin_allow_trailing_comma_on_call_site = true
ij_kotlin_assignment_wrap = normal
ij_kotlin_allow_trailing_comma = false
ij_kotlin_allow_trailing_comma_on_call_site = false
ij_kotlin_assignment_wrap = off
ij_kotlin_blank_lines_after_class_header = 0
ij_kotlin_blank_lines_around_block_when_branches = 0
ij_kotlin_blank_lines_before_declaration_with_comment_or_annotation_on_separate_line = 1
ij_kotlin_block_comment_at_first_column = true
ij_kotlin_call_parameters_new_line_after_left_paren = true
ij_kotlin_call_parameters_new_line_after_left_paren = false
ij_kotlin_call_parameters_right_paren_on_new_line = false
ij_kotlin_call_parameters_wrap = on_every_item
ij_kotlin_call_parameters_wrap = off
ij_kotlin_catch_on_new_line = false
ij_kotlin_class_annotation_wrap = split_into_lines
ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL
ij_kotlin_continuation_indent_for_chained_calls = true
ij_kotlin_continuation_indent_for_expression_bodies = true
ij_kotlin_continuation_indent_in_argument_lists = true
ij_kotlin_continuation_indent_in_elvis = false
ij_kotlin_continuation_indent_in_if_conditions = false
ij_kotlin_continuation_indent_in_parameter_lists = false
ij_kotlin_continuation_indent_in_supertype_lists = false
ij_kotlin_continuation_indent_in_elvis = true
ij_kotlin_continuation_indent_in_if_conditions = true
ij_kotlin_continuation_indent_in_parameter_lists = true
ij_kotlin_continuation_indent_in_supertype_lists = true
ij_kotlin_else_on_new_line = false
ij_kotlin_enum_constants_wrap = off
ij_kotlin_extends_list_wrap = normal
ij_kotlin_extends_list_wrap = off
ij_kotlin_field_annotation_wrap = split_into_lines
ij_kotlin_finally_on_new_line = false
ij_kotlin_if_rparen_on_new_line = false
ij_kotlin_import_nested_classes = false
ij_kotlin_imports_layout = *,java.**,javax.**,kotlin.**,^
ij_kotlin_insert_whitespaces_in_simple_one_line_method = true
ij_kotlin_keep_blank_lines_before_right_brace = 2
ij_kotlin_keep_blank_lines_in_code = 2
@ -57,13 +519,13 @@ ij_kotlin_lbrace_on_next_line = false
ij_kotlin_line_comment_add_space = false
ij_kotlin_line_comment_at_first_column = true
ij_kotlin_method_annotation_wrap = split_into_lines
ij_kotlin_method_call_chain_wrap = normal
ij_kotlin_method_parameters_new_line_after_left_paren = true
ij_kotlin_method_parameters_right_paren_on_new_line = true
ij_kotlin_method_parameters_wrap = on_every_item
ij_kotlin_name_count_to_use_star_import = 9999
ij_kotlin_name_count_to_use_star_import_for_members = 9999
ij_java_names_count_to_use_import_on_demand = 9999
ij_kotlin_method_call_chain_wrap = off
ij_kotlin_method_parameters_new_line_after_left_paren = false
ij_kotlin_method_parameters_right_paren_on_new_line = false
ij_kotlin_method_parameters_wrap = off
ij_kotlin_name_count_to_use_star_import = 5
ij_kotlin_name_count_to_use_star_import_for_members = 3
ij_kotlin_packages_to_use_import_on_demand = java.util.*,kotlinx.android.synthetic.**,io.ktor.**
ij_kotlin_parameter_annotation_wrap = off
ij_kotlin_space_after_comma = true
ij_kotlin_space_after_extend_colon = true
@ -90,5 +552,72 @@ ij_kotlin_spaces_around_when_arrow = true
ij_kotlin_variable_annotation_wrap = off
ij_kotlin_while_on_new_line = false
ij_kotlin_wrap_elvis_expressions = 1
ij_kotlin_wrap_expression_body_functions = 1
ij_kotlin_wrap_expression_body_functions = 0
ij_kotlin_wrap_first_method_in_call_chain = false
[{*.har,*.json}]
indent_size = 2
ij_json_keep_blank_lines_in_code = 0
ij_json_keep_indents_on_empty_lines = false
ij_json_keep_line_breaks = true
ij_json_space_after_colon = true
ij_json_space_after_comma = true
ij_json_space_before_colon = true
ij_json_space_before_comma = false
ij_json_spaces_within_braces = false
ij_json_spaces_within_brackets = false
ij_json_wrap_long_lines = false
[{*.htm,*.html,*.sht,*.shtm,*.shtml}]
ij_html_add_new_line_before_tags = body,div,p,form,h1,h2,h3
ij_html_align_attributes = true
ij_html_align_text = false
ij_html_attribute_wrap = normal
ij_html_block_comment_at_first_column = true
ij_html_do_not_align_children_of_min_lines = 0
ij_html_do_not_break_if_inline_tags = title,h1,h2,h3,h4,h5,h6,p
ij_html_do_not_indent_children_of_tags = html,body,thead,tbody,tfoot
ij_html_enforce_quotes = false
ij_html_inline_tags = a,abbr,acronym,b,basefont,bdo,big,br,cite,cite,code,dfn,em,font,i,img,input,kbd,label,q,s,samp,select,small,span,strike,strong,sub,sup,textarea,tt,u,var
ij_html_keep_blank_lines = 2
ij_html_keep_indents_on_empty_lines = false
ij_html_keep_line_breaks = true
ij_html_keep_line_breaks_in_text = true
ij_html_keep_whitespaces = false
ij_html_keep_whitespaces_inside = span,pre,textarea
ij_html_line_comment_at_first_column = true
ij_html_new_line_after_last_attribute = never
ij_html_new_line_before_first_attribute = never
ij_html_quote_style = double
ij_html_remove_new_line_before_tags = br
ij_html_space_after_tag_name = false
ij_html_space_around_equality_in_attribute = false
ij_html_space_inside_empty_tag = false
ij_html_text_wrap = normal
ij_html_uniform_ident = false
[{*.markdown,*.md}]
ij_markdown_force_one_space_after_blockquote_symbol = true
ij_markdown_force_one_space_after_header_symbol = true
ij_markdown_force_one_space_after_list_bullet = true
ij_markdown_force_one_space_between_words = true
ij_markdown_keep_indents_on_empty_lines = false
ij_markdown_max_lines_around_block_elements = 1
ij_markdown_max_lines_around_header = 1
ij_markdown_max_lines_between_paragraphs = 1
ij_markdown_min_lines_around_block_elements = 1
ij_markdown_min_lines_around_header = 1
ij_markdown_min_lines_between_paragraphs = 1
[{*.yaml,*.yml}]
indent_size = 2
ij_yaml_align_values_properties = do_not_align
ij_yaml_autoinsert_sequence_marker = true
ij_yaml_block_mapping_on_new_line = false
ij_yaml_indent_sequence_value = true
ij_yaml_keep_indents_on_empty_lines = false
ij_yaml_keep_line_breaks = true
ij_yaml_sequence_on_new_line = false
ij_yaml_space_before_colon = false
ij_yaml_spaces_within_braces = true
ij_yaml_spaces_within_brackets = true

View file

@ -1,2 +0,0 @@
# Ignore initial spotlessApply using ktfmt
51e9bfc67f19e16a69790a8d92bd6b1c86a76a5f

View file

@ -1,35 +0,0 @@
---
name: CLI Application
about: Report an issue with the pgpainless-cli utility
title: ''
labels: 'module: cli'
assignees: ''
---
**Describe the bug**
<!-- A clear and concise description of what the bug is. -->
**Version**
<!-- What version of the software are you using? -->
- `pgpainless-cli`:
**Installation Source**
<!-- Where did you install / build pgpainless-cli from? -->
- Debian Repository
- Built locally (`gradle build...`)
**To Reproduce**
<!-- Steps to reproduce the behavior: -->
1. `pgpainless-cli foo bar [...]`'
2. ...
**Expected behavior**
<!-- A clear and concise description of what you expected to happen. -->
**Additional context**
<!-- Add any other context (test keys, test messages) about the problem here. -->
```
-----BEGIN PGP FOO BAR-----
...
```

View file

@ -1,28 +0,0 @@
---
name: Library
about: Report an issue with the libraries pgpainless-core or pgpainless-sop
title: ''
labels: 'module: core'
assignees: ''
---
**Describe the bug**
<!-- A clear and concise description of what the bug is. -->
**Version**
<!-- What version of the software are you using? Delete lines which are not applicable. -->
- `pgpainless-core`:
- `pgpainless-sop`:
**To Reproduce**
<!-- Steps to reproduce the behavior: -->
```
Example Code Block with your Code
```
**Expected behavior**
<!-- A clear and concise description of what you expected to happen. -->
**Additional context**
<!-- Add any other context about the problem here. -->

View file

@ -17,10 +17,10 @@ name: "CodeQL"
on:
push:
branches: [ main, release/* ]
branches: [ master, release/* ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ main ]
branches: [ master ]
schedule:
- cron: '16 10 * * 0'
@ -36,7 +36,7 @@ jobs:
strategy:
fail-fast: false
matrix:
language: [ 'java-kotlin' ]
language: [ 'java' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://git.io/codeql-language-support
@ -46,7 +46,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@ -57,7 +57,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v3
uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@ -71,4 +71,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
uses: github/codeql-action/analyze@v1

View file

@ -1,27 +0,0 @@
# SPDX-FileCopyrightText: 2023 Paul Schaub <info@pgpainless.org>
#
# SPDX-License-Identifier: Apache-2.0
name: Dependencies
on:
push:
jobs:
build:
name: Dependencies
runs-on: ubuntu-latest
permissions: # The Dependency Submission API requires write permission
contents: write
steps:
- name: 'Checkout Repository'
uses: actions/checkout@v3
- name: Run snapshot action
uses: mikepenz/gradle-dependency-submission@v0.8.6
with:
gradle-build-module: |-
:pgpainless-core
:pgpainless-sop
:pgpainless-cli
sub-module-mode: |-
INDIVIDUAL_DEEP

View file

@ -9,7 +9,7 @@
# This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time
# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle
name: Push
name: Build
on:
push:
@ -28,13 +28,9 @@ jobs:
with:
java-version: '11'
distribution: 'temurin'
- name: Build and Check
uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1
with:
arguments: check jacocoRootReport
- name: Coveralls
- name: Build, Check and Coverage
uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1
env:
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
with:
arguments: coveralls
arguments: check jacocoRootReport coveralls

View file

@ -1,33 +0,0 @@
# SPDX-FileCopyrightText: 2021 Paul Schaub <info@pgpainless.org>
#
# SPDX-License-Identifier: Apache-2.0
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
# This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time
# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle
name: PR
on:
pull_request:
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK 11
uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'temurin'
- name: Build, Check and Coverage
uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1
with:
arguments: check jacocoRootReport

71
.reuse/dep5 Normal file
View file

@ -0,0 +1,71 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: PGPainless
Upstream-Contact: Paul Schaub <info@pgpainless.org>
Source: https://pgpainless.org
# Sample paragraph, commented out:
#
# Files: src/*
# Copyright: $YEAR $NAME <$CONTACT>
# License: ...
# Documentation
Files: docs/*
Copyright: 2022 Paul Schaub <info@pgpainless.org>
License: CC-BY-3.0
Files: .readthedocs.yaml
Copyright: 2022 Paul Schaub <info@pgpainless.org>
License: CC0-1.0
# Gradle build tool
Files: gradle*
Copyright: 2015 the original author or authors.
License: Apache-2.0
# PGPainless Logo
Files: assets/repository-open-graph.png
Copyright: 2021 Paul Schaub <info@pgpainless.org>
License: CC-BY-3.0
Files: assets/pgpainless.svg
Copyright: 2021 Paul Schaub <info@pgpainless.org>
License: CC-BY-3.0
Files: assets/logo.png
Copyright: 2022 Paul Schaub <info@pgpainless.org>
License: CC-BY-3.0
Files: assets/test_vectors/*
Copyright: 2018 Paul Schaub <info@pgpainless.org>
License: CC0-1.0
Files: pgpainless-core/src/test/resources/*
Copyright: 2020 Paul Schaub <info@pgpainless.org>
License: CC0-1.0
Files: audit/*
Copyright: 2021 Paul Schaub <info@pgpainless.org>
License: CC0-1.0
# GH Pages
Files: CNAME
Copyright: 2022 Paul Schaub <info@pgpainless.org>
License: CC0-1.0
Files: _config.yml
Copyright: 2022 Paul Schaub <info@pgpainless.org>
License: CC0-1.0
Files: _layouts/*
Copyright: 2022 Paul Schaub <info@pgpainless.org>, 2017 Steve Smith
License: CC-BY-SA-3.0
# Man Pages
Files: pgpainless-cli/rewriteManPages.sh
Copyright: 2022 Paul Schaub <info@pgpainless.org>
License: Apache-2.0
Files: pgpainless-cli/packaging/man/*
Copyright: 2022 Paul Schaub <info@pgpainless.org>
License: Apache-2.0

View file

@ -5,311 +5,6 @@ SPDX-License-Identifier: CC0-1.0
# PGPainless Changelog
## 1.7.7-SNAPSHOT
- Bump `bcpg-jdk8on` to `1.81`
- Bump `bcprov-jdk18on` to `1.81`
## 1.7.6
- Fix `RevocationSignatureBuilder` properly calculating third-party signatures of type `KeyRevocation` (delegation revocations)
- Enable support for native images
- Re-enable shadow plugin and build fat-jar
## 1.7.5
- Actually attempt to fix Kotlin desugaring.
- Bump javaSourceCompatibility and javaTargetCompatibility to 11
- Bump gradle-wrapper to 8.8
## 1.7.4
- Fix proper Kotlin desugaring for Java 8
## 1.7.3
- Bump `bcpg-jdk8on` to `1.80`
- Bump `bcprov-jdk18on` to `1.80`
- Add dependency on `bcutil-jdk18on` as a workaround
- Ignore unknown type signatures on certificates
- Fix typo on signature creation bounds check (thanks @elduffy)
- Fix superfluous newline added in CRLF encoding (thanks @bjansen)
- Bump `sop-java` to `1.10.0`
- SOP inline-sign: Do not apply compression
## 1.7.2
- Fix bug in `KeyRingInfo.lastModified` (thanks to @Jerbell, @sosnovsky for reporting)
- Bump `sop-java` to `10.0.3`
- allow multiple arguments `--with-key-password` in `revoke-key` command
- Properly pass `--old-key-password` and `--new-key-password` options as indirect arguments in `change-key-password` command
## 1.7.1
- Bump `sop-java` to `10.0.2`
- Downgrade `logback-core` and `logback-classic` to `1.2.13` (fix CLI spam)
## 1.7.0
- Bump `bcpg-jdk8on` to `1.78.1`
- Bump `bcprov-jdk18on` to `1.78.1`
- Bump `logback-core` and `logback-classic` to `1.4.14`
- `pgpainless-core`
- Rewrote most of the codebase in Kotlin
- Removed `OpenPgpMetadata` (`decryptionStream.getResult()`) in favor of `MessageMetadata` (`decryptionStream.getMetadata()`)
- Removed support for generating EC keys over non-standard curve `secp256k1`
- Properly feed EOS tokens to the pushdown automaton when reaching the end of stream (thanks @iNPUTmice)
- Do not choke on unknown signature subpackets (thanks @Jerbell)
- Prevent timing issues resulting in subkey binding signatures predating the subkey (@thanks Jerbell)
- Rename LibrePGP-related `Feature` enums:
- `GNUPG_AEAD_ENCRYPTED_DATA` -> `LIBREPGP_OCB_ENCRYPTED_DATA`
- `GNUPG_VERSION_5_PUBLIC_KEY` -> `LIBREPGP_VERSION_5_PUBLIC_KEY`
- Properly reject signatures by non-signing primary keys
- Add `EncryptionBuilder.discardOutput()` (useful for detached signing)
- Remove support for generation of keys over non-standard `secp256k1` curve
- Add base support for padding packets
- Do not choke on LibrePGP OED packets
- Supersede `addPassphrase()`/`addDecryptionPassphrase()` methods with more clear `addMessagePassphrase()`
- `pgpainless-sop`, `pgpainless-cli`
- Bump `sop-java` to `10.0.1`, implementing [SOP Spec Revision 10](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-10.html)
- Change API of `sop.encrypt` to return a `ReadyWithResult<EncryptionResult>` to expose the session key
- `decrypt --verify-with`: Fix to not throw `NoSignature` exception (exit code 3) if `VERIFICATIONS` is empty
- Separate signature verification operations into `SOPV` interface
- Add `version --sopv` option
- Throw `BadData` error when passing KEYS where CERTS are expected.
- `armor`: Remove `--label` option
## 1.6.8
- Bump `sop-java` to `7.0.2`
- SOP `change-key-password`: Fix reading password from indirect parameter instead of erroneously passing filename (fixes #453)
- SOP `revoke-key`: Allow for multiple `--with-key-password` options
## 1.6.7
- SOP: Fix OOM error when detached-signing large amounts of data (fix #432)
- Move `CachingBcPublicKeyDataDecryptorFactory` from `org.bouncycastle` packet to `org.pgpainless.decryption_verification` to avoid package split (partially addresses #428)
- Basic support for Java Modules for `pgpainless-core` and `pgpainless-sop`
- Added `Automatic-Module-Name` directive to gradle build files
## 1.6.6
- Downgrade `logback-core` and `logback-classic` to `1.2.13` to fix #426
## 1.6.5
- Add `SecretKeyRingEditor.setExpirationDateOfSubkey()`
## 1.6.4
- Bump `bcpg-jdk8on` to `1.77`
- Bump `bcprov-jdk18on` to `1.77`
- Bump `logback-core` and `logback-classic` to `1.4.13`
- Properly feed EOS tokens to the pushdown automaton when reaching the end of stream (thanks @iNPUTmice)
- Do not choke on unknown signature subpackets (thanks @Jerbell)
- Prevent timing issues resuting in subkey binding signatures predating the subkey (@thanks Jerbell)
## 1.6.3
- Bump `sop-java` to `7.0.1`
- `decrypt --verify-with`: Fix to not throw `NoSignature` exception (exit code 3) if `VERIFICATIONS` is empty
## 1.6.2
- Switch `bcpg` and `bcprov` artifacts from `-jdk15to18`variant to `-jdk18on`
- Bump `bcpg-jdk8on` to `1.76`
- Bump `bcprov-jdk18on` to `1.76`
- Add `EncryptionOptions.setAllowEncryptionWithMissingKeyFlags()` to properly allow
encrypting to legacy keys which do not carry any key flags.
- Allow overriding of reference time in `EncryptionOptions` and `SigningOptions`.
## 1.6.1
- `KeyRingBuilder`: Require UTF8 when adding user-ID via `addUserId(byte[])`
- `pgpainless-sop`: Remove dependency on jetbrains annotations
- Add `CertificateAuthority` interface to allow integration with [`pgpainless-wot`](https://github.com/pgpainless/pgpainless-wot)
- Add `EncryptionOptions.addAuthenticatableRecipients()` method
- Add `MessageMetadata.isAuthenticatablySignedBy()` method
## 1.6.0
- Bump `sop-java` to `7.0.0`, implementing [SOP Spec Revision 07](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-07.html)
- Implement `revoke-key` subcommand and API
- Implement `change-key-password` subcommand and API
- `generate-key`: Add support for new `--signing-only` option
- Move some methods related to password changing from `SecretKeyRingEditor` to `KeyRingUtils`
## 1.5.7
- Bump `sop-java` to `6.1.1`
- `decrypt --verify-with`: Fix to not throw `NoSignature` exception (exit code 3) if `VERIFICATIONS` is empty
## 1.5.6
- Bump `jacoco` to `0.8.8` (thanks @hkos)
- Ignore malformed, non-UTF8 user-IDs on certificates
- `KeyRingReader.readPublicKeyRingCollection()`: Extract and return public keys from encountered secret keys
- Add some utility methods to `KeyRingInfo`:
- `getValidSubkeys()` only returns validly bound sub-keys
- Add some utility methods to `SignatureUtils`:
- `getDelegations()` returns all third-party signatures made over the primary key
- `get3rdPartyCertificationsFor(userId)` returns all third-party certification signatures made over the given user-id
- Add some utility methods to `SignatureSubpacketsUtil`:
- `isExportable()` will return true if the signature is *not* marked as non-exportable
- `getTrustDepthOr()` returns the signatures trust-depth, or a default value if there is no trust-signature subpacket
- `getTrustAmountOr()` returns the signatures trust-amount, or a default value if there is no trust-signature subpacket
## 1.5.5
- Bump `bcpg-jdk15to18` to `1.75`
- Bump `bcprov-jdk15to18` to `1.75`
- Bump `checkstyle` to `10.12.1` to fix build dependency on [vulnerable guava](https://github.com/pgpainless/pgpainless/security/dependabot/6).
- `SecretKeyRingEditor`:
- Rename `createRevocationCertificate()` to `createRevocation()`
- Add `createMinimalRevocationCertificate()` method to generate OpenPGP v6-style self-certifying revocation certificates
## 1.5.4
- Bump `bcpg-jdk15to18` to `1.74`
- Bump `bcprov-jdk15to18` to `1.74`
- Remove unused methods from `SignatureUtils`
- Encryption: Allow anonymous recipients using wildcard key-IDs
- Add `SignatureSubpacketsUtil.getRegularExpressions()`
- Tests, tests, tests
## 1.5.3
- Fix minimal bit-strength check for signing-subkeys accidentally comparing the bit-strength of the primary key
- `SigningOptions`: Add new methods to add signatures using a single, chosen signing subkey
## 1.5.2
- Bugfix: Create proper direct-key signatures
- `KeyRingTemplates`:
- Add `rsaKeyRing()` for generating RSA keys with primary key and dedicated signing, encryption subkeys
- Reduce number of template methods by replacing `UserId`, `String` arguments with `CharSequence`
- Add `MessageMetadata.getRecipientKeyIds()`
- Work towards more null-safe API by annotating methods in `EncryptionOptions`, `SigningOptions`, `KeyRingInfo`, `PGPainless` with `@Nonnull`, `@Nullable`
- `KeyRingUtils`: Removed `removeSecretKey()` in favour of `stripSecretKey()`
- General code cleanup
- SOP: generating keys with `rfc4880` profile now generates key with primary key and subkeys
- Deprecate ElGamal key type
- Key generation: Set expiration period of 5 years by default
- Set AES-128 as default fallback symmetric algorithm
- `ProducerOptions`: Allow setting custom version header when encrypting/signing message
## 1.5.2-rc1
- Bump `sop-java` to `6.1.0`
- Normalize `OpenPgpMessageInputStream.read()` behaviour when reading past the stream
- Instead of throwing a `MalformedOpenPgpMessageException` which could throw off unsuspecting parsers,
we now simply return `-1` like every other `InputStream`.
## 1.5.1
- SOP: Emit signature `mode:{binary|text}` in `Verification` results
- core: Relax constraints on decryption subkeys to improve interoperability with broken clients
- Allow decryption with revoked keys
- Allow decryption with expired keys
- Allow decryption with erroneously addressed keys without encryption key flags
## 1.5.0
- Bump `bcpg-jdk15to18` to `1.73`
- Bump `bcprov-jdk15to18` to `1.73`
- Introduce `OpenPgpv6Fingerprint` class
- Bump `sop-java` to `5.0.0`, implementing [SOP Spec Revision 05](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-05.html)
- Add support for `list-profiles` subcommand (`generate-key` only for now)
- `generate-key`: Add support for `--profile=` option
- Add profile `draft-koch-eddsa-for-openpgp-00` which represents status quo.
- Add profile `rfc4880` which generates keys based on 4096-bit RSA.
- Bump `sop-java` to `6.0.0`, implementing [SOP Spec Revision 06](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-06.html)
- `encrypt`: Add support for `--profile=` option
- Add profile `rfc4880` to reflect status quo
- `version`: Add support for `--sop-spec` option
## 1.4.6
- Bump `sop-java` to `4.1.2`
- Fix `decrypt --verify-with` to not throw `NoSignature` exception (exit code 3) if `VERIFICAIONS` is empty.
## 1.4.5
- Bugfix: Direct-Key signatures are calculated over the signee key only, not the signer key + signee key
- Security: Fix faulty bit-strength policy check for signing subkeys
## 1.4.4
- Fix expectations on subpackets of v3 signatures (thanks @bjansen)
- Properly verify v3 signatures, which do not yet have signature subpackets, yet we required them to have
a hashed creation date subpacket.
## 1.4.3
- Bump `sop-java` to `4.1.1`
- Reuse shared test suite of `sop-java`
- Add `EncryptionOptions.hasEncryptionMethod()`
- SOP `encrypt`: Throw `MissingArg` exception if no encryption method was provided
- Fix redundant dot in exception message (thanks @DenBond7)
## 1.4.2
- Properly decrypt messages without MDC packets when `ConsumerOptions.setIgnoreMDCErrors(true)` is set
- Fix crash in `sop generate-key --with-key-password` when more than one user-id is given
- Revert integration with `pgp-certificate-store`
- Bump `sop-java` to `4.1.0`
## 1.4.1
- Add `UserId.parse()` method to parse user-ids into their components
## 1.4.0
- `sop generate-key`: Add support for keys without user-ids
- `sop inline-sign --as=clearsigned`: Make signature in TEXT mode
- Make countermeasures against [KOpenPGP](https://kopenpgp.com/) attacks configurable
- Countermeasures are now disabled by default since they are costly and have a specific threat model
- Can be enabled by calling `Policy.setEnableKeyParameterValidation(true)`
## 1.4.0-rc2
- Bump `bcpg-jdk15to18` to `1.72.3`
- Use BCs `PGPEncryptedDataList.extractSessionKeyEncryptedData()` method
to do decryption using session keys. This enables decryption of messages
without encrypted session key packets.
- Use BCs `PGPEncryptedDataList.isIntegrityProtected()` to check for integrity protection
- Depend on `pgp-certificate-store`
- Add `ConsumerOptions.addVerificationCerts(PGPCertificateStore)` to allow sourcing certificates from
e.g. a [certificate store implementation](https://github.com/pgpainless/cert-d-java).
- Make `DecryptionStream.getMetadata()` first class
- Deprecate `DecryptionStream.getResult()`
## 1.4.0-rc1
- Reimplement message consumption via new `OpenPgpMessageInputStream`
- Fix validation of prepended signatures (#314)
- Fix validation of nested signatures (#319)
- Reject malformed messages (#237)
- Utilize new `PDA` syntax verifier class
- Allow for custom message syntax via `Syntax` class
- Gracefully handle `UnsupportedPacketVersionException` for signatures
- Allow plugin decryption code (e.g. to add support for hardware-backed keys (see #318))
- Add `HardwareSecurity` utility class
- Add `GnuPGDummyKeyUtil` which can be used to mimic GnuPGs proprietary S2K extensions
for keys which were placed on hardware tokens
- Add `OpenPgpPacket` enum class to enumerate available packet tags
- Remove old decryption classes in favor of new implementation
- Removed `DecryptionStream` class and replaced with new abstract class
- Removed `DecryptionStreamFactory`
- Removed `FinalIOException`
- Removed `MissingLiteralDataException` (replaced by `MalformedOpenPgpMessageException`)
- Introduce `MessageMetadata` class as potential future replacement for `OpenPgpMetadata`.
- can be obtained via `((OpenPgpMessageInputStream) decryptionStream).getMetadata();`
- Add `CachingBcPublicKeyDataDecryptorFactory` which can be extended to prevent costly decryption
of session keys
- Fix: Only verify message integrity once
- Remove unnecessary `@throws` declarations on `KeyRingReader` methods
- Remove unnecessary `@throws` declarations on `KeyRingUtils` methods
- Add `KeyIdUtil.formatKeyId(long id)` to format hexadecimal key-ids.
- Add `KeyRingUtils.publicKeys(PGPKeyRing keys)`
- Remove `BCUtil` class
## 1.3.18
- Bump `sop-java` to `4.1.2`
- Fix `decrypt --verify-with XYZ` not throwing `NoSignature` exception (exit code 3) if `VERIFICATIONS` is empty (#415)
## 1.3.17
- Bugfix: Direct-Key signatures are calculated over the signee key only, not the signer key + signee key
- Security: Fix faulty bit-strength policy check for signing subkeys
## 1.3.16
- Bump `sop-java` to `4.1.0`
- Bump `gradlew` to `7.5`
## 1.3.15
- Fix crash in `sop generate-key --with-key-password` when more than one user-id is given
- `sop generate-key`: Allow key generation without user-ids
- `sop inline-sign --as=clearsigned`: Make signatures of type 'text' instead of 'binary'
## 1.3.14
- Bump `bcpg` to `1.72.3`
- Fix DSA key parameter check
- Use proper method to unlock private signing keys when creating detached signatures
## 1.3.13
- Bump `sop-java` to `4.0.7`
## 1.3.12
- Bump `sop-java` to `4.0.5`
- Fix: `sop inline-sign`: Adopt `--as=clearsigned` instead of `--as=cleartextsigned`
- SOP: Hide `Version: PGPainless` armor header in all armored outputs
- Fix: `sop armor`: Do not re-armor already armored data
## 1.3.11
- Fix: When verifying subkey binding signatures with embedded recycled primary
key binding signatures, do not reject signature if primary key binding

View file

@ -8,7 +8,7 @@ SPDX-License-Identifier: Apache-2.0
[![Build Status](https://github.com/pgpainless/pgpainless/actions/workflows/gradle_push.yml/badge.svg)](https://github.com/pgpainless/pgpainless/actions/workflows/gradle_push.yml)
[![Coverage Status](https://coveralls.io/repos/github/pgpainless/pgpainless/badge.svg?branch=main)](https://coveralls.io/github/pgpainless/pgpainless?branch=main)
[![Interoperability Test-Suite](https://badgen.net/badge/Sequoia%20Test%20Suite/results/green)](https://tests.sequoia-pgp.org/)
[![Interoperability Test-Suite](https://badgen.net/badge/Sequoia%20Test%20Suite/%232/green)](https://tests.sequoia-pgp.org/)
[![PGP](https://img.shields.io/badge/pgp-A027%20DB2F%203E1E%20118A-blue)](https://keyoxide.org/7F9116FEA90A5983936C7CFAA027DB2F3E1E118A)
[![REUSE status](https://api.reuse.software/badge/github.com/pgpainless/pgpainless)](https://api.reuse.software/info/github.com/pgpainless/pgpainless)
[![Documentation Status](https://readthedocs.org/projects/pgpainless/badge/?version=latest)](https://pgpainless.readthedocs.io/en/latest/?badge=latest)
@ -32,7 +32,7 @@ It also checks if signing subkeys are properly bound to their primary key, if ke
if keys are allowed to create signatures in the first place.
These rigorous checks make PGPainless stand out from other Java-based OpenPGP libraries and are the reason why
PGPainless currently [*scores first place* on Sequoia-PGPs Interoperability Test-Suite](https://tests.sequoia-pgp.org).
PGPainless currently [*scores second place* on Sequoia-PGPs Interoperability Test-Suite](https://tests.sequoia-pgp.org).
> At FlowCrypt we are using PGPainless in our Kotlin code bases on Android and on server side.
> The ergonomics of legacy PGP tooling on Java is not very good, and PGPainless improves it greatly.
@ -132,7 +132,7 @@ Still it allows you to manually specify which algorithms to use of course.
.addRecipient(aliceKey)
.addRecipient(bobsKey)
// optionally encrypt to a passphrase
.addMessagePassphrase(Passphrase.fromPassword("password123"))
.addPassphrase(Passphrase.fromPassword("password123"))
// optionally override symmetric encryption algorithm
.overrideEncryptionAlgorithm(SymmetricKeyAlgorithm.AES_192),
new SigningOptions()
@ -172,10 +172,10 @@ This behaviour can be modified though using the `Policy` class.
decryptionStream.close();
// Result contains information like signature status etc.
MessageMetadata metadata = decryptionStream.getMetadata();
OpenPgpMetadata metadata = decryptionStream.getResult();
```
*After* the `DecryptionStream` was closed, you can get metadata about the processed data by retrieving the `MessageMetadata`.
*After* the `DecryptionStream` was closed, you can get metadata about the processed data by retrieving the `OpenPgpMetadata`.
Again, this object will contain information about how the message was encrypted, who signed it and so on.
#### Many more examples can be found in the [examples package](pgpainless-core/src/test/java/org/pgpainless/example)!!!
@ -191,7 +191,7 @@ repositories {
}
dependencies {
implementation 'org.pgpainless:pgpainless-core:1.7.6'
implementation 'org.pgpainless:pgpainless-core:1.3.11'
}
```

View file

@ -1,118 +0,0 @@
version = 1
SPDX-PackageName = "PGPainless"
SPDX-PackageSupplier = "Paul Schaub <info@pgpainless.org>"
SPDX-PackageDownloadLocation = "https://pgpainless.org"
[[annotations]]
path = "REUSE.toml"
precedence = "aggregate"
SPDX-FileCopyrightText = "2025 Paul Schaub <info@pgpainless.org>"
SPDX-License-Identifier = "CC0-1.0"
[[annotations]]
path = ".git-blame-ignore-revs"
precedence = "aggregate"
SPDX-FileCopyrightText = "2023 Paul Schaub <info@pgpainless.org>"
SPDX-License-Identifier = "CC0-1.0"
[[annotations]]
path = "docs/**"
precedence = "aggregate"
SPDX-FileCopyrightText = "2022 Paul Schaub <info@pgpainless.org>"
SPDX-License-Identifier = "CC-BY-3.0"
[[annotations]]
path = ".readthedocs.yaml"
precedence = "aggregate"
SPDX-FileCopyrightText = "2022 Paul Schaub <info@pgpainless.org>"
SPDX-License-Identifier = "CC0-1.0"
[[annotations]]
path = "gradle**"
precedence = "aggregate"
SPDX-FileCopyrightText = "2015 the original author or authors."
SPDX-License-Identifier = "Apache-2.0"
[[annotations]]
path = ".editorconfig"
precedence = "aggregate"
SPDX-FileCopyrightText = "Facebook"
SPDX-License-Identifier = "Apache-2.0"
[[annotations]]
path = "assets/repository-open-graph.png"
precedence = "aggregate"
SPDX-FileCopyrightText = "2021 Paul Schaub <info@pgpainless.org>"
SPDX-License-Identifier = "CC-BY-3.0"
[[annotations]]
path = "assets/pgpainless.svg"
precedence = "aggregate"
SPDX-FileCopyrightText = "2021 Paul Schaub <info@pgpainless.org>"
SPDX-License-Identifier = "CC-BY-3.0"
[[annotations]]
path = "assets/logo.png"
precedence = "aggregate"
SPDX-FileCopyrightText = "2022 Paul Schaub <info@pgpainless.org>"
SPDX-License-Identifier = "CC-BY-3.0"
[[annotations]]
path = "assets/test_vectors/**"
precedence = "aggregate"
SPDX-FileCopyrightText = "2018 Paul Schaub <info@pgpainless.org>"
SPDX-License-Identifier = "CC0-1.0"
[[annotations]]
path = "pgpainless-core/src/test/resources/**"
precedence = "aggregate"
SPDX-FileCopyrightText = "2020 Paul Schaub <info@pgpainless.org>"
SPDX-License-Identifier = "CC0-1.0"
[[annotations]]
path = "audit/**"
precedence = "aggregate"
SPDX-FileCopyrightText = "2021 Paul Schaub <info@pgpainless.org>"
SPDX-License-Identifier = "CC0-1.0"
[[annotations]]
path = "CNAME"
precedence = "aggregate"
SPDX-FileCopyrightText = "2022 Paul Schaub <info@pgpainless.org>"
SPDX-License-Identifier = "CC0-1.0"
[[annotations]]
path = "_config.yml"
precedence = "aggregate"
SPDX-FileCopyrightText = "2022 Paul Schaub <info@pgpainless.org>"
SPDX-License-Identifier = "CC0-1.0"
[[annotations]]
path = "_layouts/**"
precedence = "aggregate"
SPDX-FileCopyrightText = "2022 Paul Schaub <info@pgpainless.org>, 2017 Steve Smith"
SPDX-License-Identifier = "CC-BY-SA-3.0"
[[annotations]]
path = "pgpainless-cli/src/main/resources/META-INF/native-image/**"
precedence = "aggregate"
SPDX-FileCopyrightText = "2025 Paul Schaub <info@pgpainless.org>"
SPDX-License-Identifier = "Apache-2.0"
[[annotations]]
path = "pgpainless-cli/rewriteManPages.sh"
precedence = "aggregate"
SPDX-FileCopyrightText = "2022 Paul Schaub <info@pgpainless.org>"
SPDX-License-Identifier = "Apache-2.0"
[[annotations]]
path = "pgpainless-cli/packaging/man/**"
precedence = "aggregate"
SPDX-FileCopyrightText = "2022 Paul Schaub <info@pgpainless.org>"
SPDX-License-Identifier = "Apache-2.0"
[[annotations]]
path = ".github/ISSUE_TEMPLATE/**"
precedence = "aggregate"
SPDX-FileCopyrightText = "2024 Paul Schaub <info@pgpainless.org>"
SPDX-License-Identifier = "CC0-1.0"

View file

@ -12,14 +12,11 @@ SPDX-License-Identifier: Apache-2.0
Use this section to tell people about which versions of your project are
currently being supported with security updates.
| Version | Supported | Note |
|---------|--------------------|------------|
| 1.7.X | :white_check_mark: | |
| 1.6.X | :white_check_mark: | LTS branch |
| 1.5.X | :white_check_mark: | |
| 1.4.X | :white_check_mark: | |
| 1.3.X | :white_check_mark: | LTS branch |
| < 1.3.X | :x: | |
| Version | Supported |
|---------| ------------------ |
| 1.1.X | :white_check_mark: |
| 1.0.X | :white_check_mark: |
| < 1.0.0 | :x: |
## Reporting a Vulnerability

View file

@ -18,8 +18,7 @@ buildscript {
}
plugins {
id 'org.jetbrains.kotlin.jvm' version "1.8.10"
id 'com.diffplug.spotless' version '6.22.0' apply false
id 'ru.vyarus.animalsniffer' version '1.5.3'
}
apply from: 'version.gradle'
@ -30,32 +29,37 @@ allprojects {
apply plugin: 'eclipse'
apply plugin: 'jacoco'
apply plugin: 'checkstyle'
apply plugin: 'kotlin'
apply plugin: 'com.diffplug.spotless'
// Only generate jar for submodules
// without this we would generate an empty pgpainless.jar for the project root
// https://stackoverflow.com/a/25445035
jar {
reproducibleFileOrder = true
onlyIf { !sourceSets.main.allSource.files.isEmpty() }
}
// For library modules, enable android api compatibility check
if (it.name != 'pgpainless-cli') {
// animalsniffer
apply plugin: 'ru.vyarus.animalsniffer'
dependencies {
signature "net.sf.androidscents.signature:android-api-level-${pgpainlessMinAndroidSdk}:2.3.3_r2@signature"
}
animalsniffer {
sourceSets = [sourceSets.main]
}
}
// checkstyle
checkstyle {
toolVersion = '10.25.0'
}
spotless {
kotlin {
ktfmt().dropboxStyle()
}
toolVersion = '8.18'
}
group 'org.pgpainless'
description = "Simple to use OpenPGP API for Java based on Bouncycastle"
version = shortVersion
sourceCompatibility = javaSourceCompatibility
repositories {
mavenCentral()
mavenLocal()
@ -70,17 +74,6 @@ allprojects {
fileMode = 0644
}
kotlin {
jvmToolchain(javaSourceCompatibility)
}
// Compatibility of default implementations in kotlin interfaces with Java implementations.
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
kotlinOptions {
freeCompilerArgs += ["-Xjvm-default=all-compatibility"]
}
}
project.ext {
rootConfigDir = new File(rootDir, 'config')
gitCommit = getGitCommit()
@ -106,7 +99,7 @@ allprojects {
}
jacoco {
toolVersion = "0.8.8"
toolVersion = "0.8.7"
}
jacocoTestReport {
@ -114,7 +107,7 @@ allprojects {
sourceDirectories.setFrom(project.files(sourceSets.main.allSource.srcDirs))
classDirectories.setFrom(project.files(sourceSets.main.output))
reports {
xml.required = true
xml.enabled true
}
}
@ -132,15 +125,15 @@ subprojects {
apply plugin: 'signing'
task sourcesJar(type: Jar, dependsOn: classes) {
archiveClassifier = 'sources'
classifier = 'sources'
from sourceSets.main.allSource
}
task javadocJar(type: Jar, dependsOn: javadoc) {
archiveClassifier = 'javadoc'
classifier = 'javadoc'
from javadoc.destinationDir
}
task testsJar(type: Jar, dependsOn: testClasses) {
archiveClassifier = 'tests'
classifier = 'tests'
from sourceSets.test.output
}
@ -237,7 +230,7 @@ task jacocoRootReport(type: JacocoReport) {
classDirectories.setFrom(files(subprojects.sourceSets.main.output))
executionData.setFrom(files(subprojects.jacocoTestReport.executionData))
reports {
xml.required = true
xml.enabled true
xml.destination file("${buildDir}/reports/jacoco/test/jacocoTestReport.xml")
}
// We could remove the following setOnlyIf line, but then
@ -248,6 +241,10 @@ task jacocoRootReport(type: JacocoReport) {
}
task javadocAll(type: Javadoc) {
def currentJavaVersion = JavaVersion.current()
if (currentJavaVersion.compareTo(JavaVersion.VERSION_1_9) >= 0) {
options.addStringOption("-release", "8");
}
source subprojects.collect {project ->
project.sourceSets.main.allJava }
destinationDir = new File(buildDir, 'javadoc')
@ -260,23 +257,3 @@ task javadocAll(type: Javadoc) {
"https://docs.oracle.com/javase/${sourceCompatibility.getMajorVersion()}/docs/api/",
] as String[]
}
/**
* Fetch sha256 checksums of artifacts published to maven central.
*
* Example: gradle -Prelease=1.3.13 mavenCentralChecksums
*/
task mavenCentralChecksums() {
description 'Fetch and display checksums for artifacts published to Maven Central'
String ver = project.hasProperty('release') ? release : shortVersion
doLast {
for (Project p : rootProject.subprojects) {
String url = "https://repo1.maven.org/maven2/org/pgpainless/${p.name}/${ver}/${p.name}-${ver}.jar.sha256"
Process fetch = "curl -f $url".execute()
if (fetch.waitFor() == 0) {
print fetch.text.trim()
println " ${p.name}/build/libs/${p.name}-${ver}.jar"
}
}
}
}

View file

@ -114,12 +114,18 @@ SPDX-License-Identifier: CC0-1.0
</module>
<module name="JavadocMethod">
<property name="accessModifiers" value="public"/>
<!-- TODO stricten those checks -->
<property name="scope" value="public"/>
<!--<property name="allowUndeclaredRTE" value="true"/>-->
<property name="allowMissingParamTags" value="true"/>
<property name="allowMissingThrowsTags" value="true"/>
<property name="allowMissingReturnTag" value="true"/>
<property name="allowMissingJavadoc" value="true"/>
<property name="suppressLoadErrors" value="true"/>
</module>
<module name="JavadocStyle">
<property name="scope" value="public"/>
<property name="checkEmptyJavadoc" value="true"/>
<property name="checkHtml" value="false"/>
</module>

View file

@ -1,3 +1,2 @@
myst-parser>=0.17
sphinxcontrib-mermaid>=0.7.1
sphinx_rtd_theme>=2.0.0

View file

@ -47,13 +47,6 @@ The diagram below shows, how the different projects relate to one another.
* `pgpainless-cert-d` - PGPainless-based implementation of `pgp-cert-d-java`
* `pgpainless-cert-d-cli` - CLI frontend for `pgpainless-cert-d`
* {{ '[PGPainless-WOT](https://{}/pgpainless/pgpainless-wot)'.format(repo_host) }}
Implementation of the [OpenPGP Web of Trust specification](https://sequoia-pgp.gitlab.io/sequoia-wot/) using PGPainless.
* `pgpainless-wot` - Parse OpenPGP keyrings into a generic `Network` object
* `wot-dijkstra` - Perform queries to find paths inside a `Network` object
* `pgpainless-wot-cli` - CLI frontend for `pgpainless-wot` and `wot-dijkstra`
* `wot-test-suite` - Test vectors ported from [Sequoia-PGPs WoT implementation](https://gitlab.com/sequoia-pgp/sequoia-wot/-/tree/main/tests/data)
* {{ '[PGPeasy](https://{}/pgpainless/pgpeasy)'.format(repo_host) }}
Prototypical, comprehensive OpenPGP CLI application
* `pgpeasy` - CLI application

View file

@ -27,13 +27,6 @@ flowchart LR
subgraph VKS-JAVA
vks-java-cli-->vks-java
end
subgraph PGPAINLESS-WOT
wot-test-suite-->pgpainless-wot
pgpainless-wot-->wot-dijkstra
pgpainless-wot-cli-->pgpainless-wot
pgpainless-wot-->pgpainless-core
pgpainless-wot-cli-->pgpainless-cert-d
end
subgraph PGPEASY
pgpeasy-->pgpainless-cli
pgpeasy-->wkd-java-cli

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 108 KiB

Before After
Before After

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Before After
Before After

View file

@ -42,36 +42,6 @@ $ gradle installDist
Afterwards, an uncompressed distributable is installed in `build/install/`.
To execute the application, you can call `build/install/bin/pgpainless-cli{.bat}`
Building / updating man pages is a two-step process.
The contents of the man pages is largely defined by the `sop-java-picocli` source code.
In order to generate a fresh set of man pages from the `sop-java-picocli` source, you need to clone that repository
next to the `pgpainless` repository:
```shell
$ ls
pgpainless
$ git clone https://github.com/pgpainless/sop-java.git
$ ls
pgpainless sop-java
```
Next, you need to execute the `asciiDoctor` gradle task inside the sop-java repository:
```shell
$ cd sop-java
$ gradle asciiDoctor
```
This will generate generic sop manpages in `sop-java-picocli/build/docs/manpage/`.
Next, you need to execute a script for converting the `sop` manpages to fit the `pgpainless-cli` command with the help
of a script in the `pgpainless` repository:
```shell
$ cd ../pgpainless/pgpainless-cli
$ ./rewriteManPages.sh
```
The resulting updated man pages are placed in `packaging/man/`.
## Usage
Hereafter, the program will be referred to as `pgpainless-cli`.
@ -79,32 +49,26 @@ Hereafter, the program will be referred to as `pgpainless-cli`.
```
$ pgpainless-cli help
Stateless OpenPGP Protocol
Usage: pgpainless-cli [--stacktrace] [COMMAND]
Options:
--stacktrace Print stacktrace
Usage: pgpainless-cli [COMMAND]
Commands:
version Display version information about the tool
list-profiles Emit a list of profiles supported by the identified
subcommand
generate-key Generate a secret key
change-key-password Update the password of a key
revoke-key Generate revocation certificates
extract-cert Extract a public key certificate from a secret key
sign Create a detached message signature
verify Verify a detached signature
encrypt Encrypt a message from standard input
decrypt Decrypt a message
inline-detach Split signatures from a clearsigned message
inline-sign Create an inline-signed message
inline-verify Verify an inline-signed message
armor Add ASCII Armor to standard input
dearmor Remove ASCII Armor from standard input
help Display usage information for the specified subcommand
help Display usage information for the specified subcommand
armor Add ASCII Armor to standard input
dearmor Remove ASCII Armor from standard input
decrypt Decrypt a message from standard input
inline-detach Split signatures from a clearsigned message
encrypt Encrypt a message from standard input
extract-cert Extract a public key certificate from a secret key from
standard input
generate-key Generate a secret key
sign Create a detached signature on the data from standard input
verify Verify a detached signature over the data from standard input
inline-sign Create an inline-signed message from data on standard input
inline-verify Verify inline-signed data from standard input
version Display version information about the tool
Exit Codes:
0 Successful program execution
0 Successful program execution.
1 Generic program error
3 Verification requested but no verifiable signature found
13 Unsupported asymmetric algorithm
@ -120,44 +84,8 @@ Exit Codes:
61 Input file does not exist
67 Cannot unlock password protected secret key
69 Unsupported subcommand
71 Unsupported special prefix (e.g. "@ENV/@FD") of indirect parameter
71 Unsupported special prefix (e.g. "@env/@fd") of indirect parameter
73 Ambiguous input (a filename matching the designator already exists)
79 Key is not signing capable
83 Options were supplied that are incompatible with each other
89 The requested profile is unsupported, or the indicated subcommand does
not accept profiles
```
To get help on a subcommand, e.g. `encrypt`, just call the help subcommand followed by the subcommand you
are interested in (e.g. `pgpainless-cli help encrypt`).
## Examples
```shell
$ # Generate a key
$ pgpainless-cli generate-key "Alice <alice@pgpainless.org>" > key.asc
$ # Extract a certificate from a key
$ cat key.asc | pgpainless-cli extract-cert > cert.asc
$ # Create an encrypted signed message
$ echo "Hello, World!" | pgpainless-cli encrypt cert.asc --sign-with key.asc > msg.asc
$ # Decrypt an encrypted message and verify the signature
$ cat msg.asc | pgpainless-cli decrypt key.asc --verify-with cert.asc --verifications-out verifications.txt
Hello, World!
$ cat verifications.txt
2022-11-15T21:25:48Z 4FF67C69150209ED8139DE22578CB2FABD5D7897 9000235358B8CEA6A368EC86DE56DC2D942ACAA4
```
## Indirect Data Types
Some commands take options whose arguments are indirect data types. Those are arguments which are not used directly,
but instead they point to a place where the argument value can be sourced from, such as a file, an environment variable
or a file descriptor.
It is important to keep in mind, that options like `--with-password` or `--with-key-password` are examples for such
indirect data types. If you want to unlock a key whose password is `sw0rdf1sh`, you *cannot* provide the password
like `--with-key-password sw0rdf1sh`, but instead you have to either write out the password into a file and provide
the file's path (e.g. `--with-key-password /path/to/file`), store the password in an environment variable and pass that
(e.g. `--with-key-password @ENV:myvar`), or provide a numbered file descriptor from which the password can be read
(e.g. `--with-key-password @FD:4`).
Note, that environment variables and file descriptors can only be used to pass input data to the program.
For output parameters (e.g. `--verifications-out`) only file paths are allowed.
Powered by picocli
```

View file

@ -50,15 +50,9 @@ There is a very good chance that you can find code examples there that fit your
Reading keys from ASCII armored strings or from binary files is easy:
```java
// Secret Keys
String key = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"...;
PGPSecretKeyRing secretKey = PGPainless.readKeyRing()
.secretKeyRing(key);
// Certificates (Public Keys)
String cert = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n...";
PGPPublicKeyRing certificate = PGPainless.readKeyRing()
.publicKeyRing(cert);
```
Similarly, keys or certificates can quickly be exported:
@ -237,15 +231,6 @@ EncryptionOptions encOptions = EncryptionOptions.get()
Once again, it is possible to add multiple recipients by repeating the `addRecipient()` method call.
In order to prevent metadata leaks, you might want to add recipients anonymously.
Anonymous recipients have their key-id hidden by replacing it with a wildcard.
That way, it is not easily possible for an attacker to deduce the recipients of a message without further
analysis of additional metadata.
Anonymous recipients can be added like follows:
```java
encOptions.addHiddenRecipient(certificate);
```
You can also encrypt a message to a password like this:
```java
encOptions.addPassphrase(Passphrase.fromPassword("sw0rdf1sh"));
@ -257,7 +242,7 @@ or the passphrase.
### Decrypt and/or Verify a Message
Decryption and verification of a message is both done using the same API.
Whether a message was actually signed / encrypted can be determined after the message has been processed by checking
the `MessageMetadata` object which can be obtained from the `DecryptionStream`.
the `OpenPgpMetadata` object which can be obtained from the `DecryptionStream`.
To configure the decryption / verification process, the `ConsumerOptions` object is used:
@ -298,16 +283,16 @@ Streams.pipeAll(consumerStream, plaintext);
consumerStream.close(); // important!
// The result will contain metadata of the message
MessageMetadata result = consumerStream.getMetadata();
OpenPgpMetadata result = consumerStream.getResult();
```
After the message has been processed, you can consult the `MessageMetadata` object to determine the nature of the message:
After the message has been processed, you can consult the `OpenPgpMetadata` object to determine the nature of the message:
```java
boolean wasEncrypted = result.isEncrypted();
SubkeyIdentifier decryptionKey = result.getDecryptionKey();
List<SignatureVerification> validSignatures = result.getVerifiedSignatures();
boolean wasSignedByCert = result.isVerifiedSignedBy(certificate);
Map<SubkeyIdentifier, PGPSignature> validSignatures = result.getVerifiedSignatures();
boolean wasSignedByCert = result.containsVerifiedSignatureFrom(certificate);
// For files:
String fileName = result.getFileName();
@ -338,168 +323,6 @@ DecryptionStream verificationStream = PGPainless.decryptAndOrVerify()
Streams.drain(verificationStream); // push all the data through the stream
verificationStream.close(); // finish verification
MessageMetadata result = verificationStream.getMetadata(); // get metadata of signed message
assertTrue(result.isVerifiedSignedBy(certificate)); // check if message was in fact signed
```
### Legacy Compatibility
Out of the box, PGPainless is configured to use secure defaults and perform checks for recommended
security features. This means that for example messages generated using older OpenPGP
implementations which do not follow those best practices might fail to decrypt/verify.
It is however possible to circumvent certain security checks to allow processing of such messages.
:::{note}
It is not recommended to disable security checks, as that might enable certain attacks on the OpenPGP protocol.
:::
#### Missing / broken MDC (modification detection code)
RFC4880 has two different types of encrypted data packets. The *Symmetrically Encrypted Data* packet (SED) and the *Symmetrically Encrypted Integrity-Protected Data* packet.
The latter has an added MDC packet which prevents modifications to the ciphertext.
While implementations are highly encouraged to only use the latter package type, some older implementations still generate
encrypted data packets which are not integrity protected.
To allow PGPainless to decrypt such messages, you need to set a flag in the `ConsumerOptions` object:
```java
ConsumerOptions options = ConsumerOptions.get()
.setIgnoreMDCErrors(true) // <-
.setDecryptionKey(secretKey)
...
DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify()
.onInputStream(ciphertextIn)
.withOptions(options);
...
```
:::{note}
It is highly advised to only set this flag if you know what you are doing.
It might also be a good idea to try decrypting a message without the flag set first and only re-try
decryption with the flag set in case of a `MessageNotIntegrityProtectedException` (don't forget to rewind the ciphertextInputStream).
:::
#### Weak keys and broken algorithms
Some users might cling on to older keys using weak algorithms / small key sizes.
PGPainless refuses to encrypt to weak certificates and sign with weak keys.
By default, PGPainless follows the recommendations for acceptable key sizes of [the German BSI in 2021](https://www.bsi.bund.de/SharedDocs/Downloads/EN/BSI/Publications/TechGuidelines/TG02102/BSI-TR-02102-1.pdf).
It can however be configured to accept older key material / algorithms too.
Minimal key lengths can be configured by changing PGPainless' policy:
```java
Map<PublicKeyAlgorithm, Integer> algorithms = new HashMap<>();
// put all acceptable algorithms and their minimal key length
algorithms.put(PublicKeyAlgorithm.RSA_GENERAL, 1024);
algorithms.put(PublicKeyAlgorithm.ECDSA, 100);
...
Policy.PublicKeyAlgorithmPolicy pkPolicy =
new Policy.PublicKeyAlgorithmPolicy(algorithms);
// set the custom algorithm policy
PGPainless.getPolicy().setPublicKeyAlgorithmPolicy();
```
Since OpenPGP uses a hybrid encryption scheme of asymmetric and symmetric encryption algorithms,
it also comes with a policy for symmetric encryption algorithms.
This list can be modified to allow for weaker algorithms like follows:
```java
// default fallback algorithm for message encryption
SymmetricKeyAlgorithm fallbackAlgorithm = SymmetricKeyAlgorithm.AES_256;
// acceptable algorithms
List<SymmetricKeyAlgorithm> algorithms = new ArrayList<>();
algorithms.add(SymmetricKeyAlgorithm.AES_256);
algorithms.add(SymmetricKeyAlgorithm.AES_192);
algorithms.add(SymmetricKeyAlgorithm.AES_128);
algorithms.add(SymmetricKeyAlgorithm.TWOFISH);
algorithms.add(SymmetricKeyAlgorithm.BLOWFISH);
...
Policy.SymmetricKeyAlgorithmPolicy skPolicy =
new SymmtricKeyAlgorithmPolicy(fallbackAlgorithm, algorithms);
// set the custom algorithm policy
// algorithm policy applicable when decrypting messages created by legacy senders:
PGPainless.getPolicy()
.setSymmetricKeyDecryptionAlgorithmPolicy(skPolicy);
// algorithm policy applicable when generating messages for legacy recipients:
PGPainless.getPolicy()
.setSymmetricKeyEncryptionAlgorithmPolicy(skPolicy);
```
Hash algorithms are used in OpenPGP to create signatures.
Since signature verification is an integral part of the OpenPGP protocol, PGPainless comes
with multiple policies for acceptable hash algorithms, depending on the use-case.
Revocation signatures are critical, so you might want to handle revocation signatures differently from normal signatures.
By default, PGPainless uses a smart hash algorithm policy for both use-cases, which takes into consideration
not only the hash algorithm itself, but also the creation date of the signature.
That way, signatures using SHA-1 are acceptable if they were created before February 2013, but are rejected if their
creation date is after that point in time.
A custom hash algorithm policy can be set like this:
```java
HashAlgorithm fallbackAlgorithm = HashAlgorithm.SHA512;
Map<HashAlgorithm, Date> algorithms = new HashMap<>();
// Accept MD5 on signatures made before 1997-02-01
algorithms.put(HashAlgorithm.MD5,
DateUtil.parseUTCDate("1997-02-01 00:00:00 UTC"));
// Accept SHA-1, regardless of signature creation time
algorithms.put(HashAlgorithm.SHA1, null);
...
Policy.HashAlgorithmPolicy hPolicy =
new Policy.HashAlgorithmPolicy(fallbackAlgorithm, algorithms);
// set policy for revocation signatures
PGPainless.getPolicy()
.setRevocationSignatureHashAlgorithmPolicy(hPolicy);
// set policy for normal signatures (certifications and document signatures)
PGPainless.getPolicy()
.setSignatureHashAlgorithmPolicy(hPolicy);
```
Lastly, PGPainless comes with a policy on acceptable compression algorithms, which currently accepts any
compression algorithm.
A custom compression algorithm policy can be set in a similar way:
```java
CompressionAlgorithm fallback = CompressionAlgorithm.ZIP;
List<CompressionAlgorithm> algorithms = new ArrayList<>();
algorithms.add(CompressionAlgorith.ZIP);
algorithms.add(CompressionAlgorithm.BZIP2);
...
Policy.CompressionAlgorithmPolicy cPolicy =
new Policy.CompressionAlgorithmPolicy(fallback, algorithms);
PGPainless.getPolicy()
.setCompressionAlgorithmPolicy(cPolicy);
```
To prevent a class of attacks described in the [paper](https://www.kopenpgp.com/#paper)
"Victory by KO: Attacking OpenPGP Using Key Overwriting",
PGPainless offers the option to validate private key material each time before using it,
to make sure that an attacker didn't tamper with the corresponding public key parameters.
These checks are disabled by default, but they can be enabled as follows:
```java
PGPainless.getPolicy()
.setEnableKeyParameterValidation(true);
```
:::{note}
Validation checks against KOpenPGP attacks are disabled by default, since they are very costly
and only make sense in certain scenarios.
Please read and understand the paper to decide, if enabling the checks makes sense for your use-case.
:::
### Known Notations
In OpenPGP, signatures can contain [notation subpackets](https://www.rfc-editor.org/rfc/rfc4880#section-5.2.3.16).
A notation can give meaning to a signature, or add additional contextual information.
Signature subpackets can be marked as critical, meaning an implementation that does not know about
a certain subpacket MUST reject the signature.
The same is true for critical notations.
For that reason, PGPainless comes with a `NotationRegistry` class which can be used to register known notations,
such that a signature containing a critical notation of a certain value is not rejected.
To register a known notation, you can do the following:
```java
NotationRegistry registry = PGPainless.getPolicy()
.getNotationRegistry();
registry.addKnownNotation("sample@example.com");
OpenPgpMetadata result = verificationStream.getResult(); // get metadata of signed message
assertTrue(result.containsVerifiedSignatureFrom(certificate)); // check if message was in fact signed
```

View file

@ -29,19 +29,4 @@ UserId full = UserId.newBuilder()
.withComment("Work Address")
.build();
assertEquals("Peter Pattern (Work Address) <peter@pgpainless.org>", full.toString());
```
If you have a User-ID in form of a string (e.g. because a user provided it via a text field),
you can parse it into its components like this:
```java
String string = "John Doe <john@doe.corp>";
UserId userId = UserId.parse(string);
// Now you can access the different components
assertEquals("John Doe", userId.getName());
assertEquals("john@doe.corp", userId.getEmail());
assertNull(userId.getComment());
```
The method `UserId.parse(String string)` will throw an `IllegalArgumentException` if the User-ID is malformed.
```

View file

@ -75,21 +75,10 @@ In both cases, the resulting output will be the UTF8 encoded, ASCII armored Open
To disable ASCII armoring, call `noArmor()` before calling `generate()`.
Revision `05` of the Stateless OpenPGP Protocol specification introduced the concept of profiles for
certain operations.
The key generation feature is the first operation to make use of profiles to specify different key algorithms.
To set a profile, simply call `profile(String profileName)` and pass in one of the available profile identifiers.
To explore, which profiles are available, refer to the dedicated [section](#explore-profiles).
The default profile used by `pgpainless-sop` is called `draft-koch-eddsa-for-openpgp-00`.
If this profile is used, the resulting OpenPGP secret key will consist of a certification-capable 256-bits
At the time of writing, the resulting OpenPGP secret key will consist of a certification-capable 256-bits
ed25519 EdDSA primary key, a 256-bits ed25519 EdDSA subkey used for signing, as well as a 256-bits X25519
ECDH subkey for encryption.
Another profile defined by `pgpainless-sop` is `rfc4880`, which changes the key generation behaviour such that
the resulting key is a single 4096-bit RSA key capable of certifying, signing and encrypting.
The whole key does not have an expiration date set.
### Extract a Certificate
@ -114,56 +103,6 @@ To disable ASCII armoring, call `noArmor()` before calling `key(_)`.
In our example, `certificateBytes` can now safely be shared with anyone.
### Change Key Password
OpenPGP keys can (but don't need to) be password protected.
The `changeKeyPassword()` API can be used to add, change or remove password protection from OpenPGP keys.
While the input to this operation can be keys with different per-subkey passwords, the output will use at most one password.
Via `oldKeyPassphrase()`, multiple decryption passphrase candidates can be provided.
These are tried one after another to unlock protected subkeys.
In order to successfully change the passphrase of an OpenPGP key, all of its subkeys needs to be successfully decrypted.
If one or more subkeys cannot be decrypted, the operation fails with a `KeyIsProtected` exception.
The result is either fully encrypted for a single passphrase (passed via `newKeyPassphrase()`),
or unprotected if the new key passphrase is omitted.
```java
byte[] keyBefore = ...
byte[] keyAfter = sop.changeKeyPassword()
// Provide old passphrases - all subkeys need to be decryptable,
// otherwise KeyIsProtected exception will be thrown
.oldKeyPassphrase("4d4m5m1th")
.oldKeyPassphrase("d4v1dR1c4rd0")
// Provide the new passphrase - if omitted, key will be unprotected
.newKeyPassphrase("fr1edr1ch3n93l5")
.keys(keyBefore)
.getBytes();
```
### Generate Revocation Certificates
You might want to generate a revocation certificate for your OpenPGP key.
This certificate can be published to a key server to let your contacts known that your key is no longer
trustworthy.
The `revokeKey()` API can be used to generate a "hard-revocation", which retroactively invalidates all
signatures previously issued by the key.
If the input secret key is an OpenPGP v6 key, the result will be a minimal revocation certificate,
consisting of only the bare primary public key and a revocation signature. For v4 keys, the result
will consist of the whole public certificate plus a revocation signature.
```java
byte[] keys = ...
byte[] revoked = sop.revokeKey()
// primary key password(s) if the key(s) are protected
.withKeyPassword("5w0rdf1sh")
// one or more secret keys
.keys(keys)
.getBytes();
```
### Apply / Remove ASCII Armor
Perhaps you want to print your secret key onto a piece of paper for backup purposes,
@ -180,6 +119,14 @@ byte[] armoredData = sop.armor()
The `data(_)` method can either be called by providing a byte array, or an `InputStream`.
:::{note}
There is a `label(ArmorLabel label)` method, which could theoretically be used to define the label used in the
ASCII armor header.
However, this method is not (yet?) supported by `pgpainless-sop` and will currently throw an `UnsupportedOption`
exception.
Instead, the implementation will figure out the data type and set the respective label on its own.
:::
To remove ASCII armor from armored data, simply use the `dearmor()` API:
```java
@ -239,13 +186,6 @@ If any keys used for signing are password protected, you need to provide the sig
It does not matter in which order signing keys and key passwords are provided, the implementation will figure out
matches on its own. If different key passwords are used, the `withKeyPassword(_)` method can be called multiple times.
You can modify the behaviour of the encrypt operation by switching between different profiles via the
`profile(String profileName)` method.
At the time of writing, the only available profile for this operation is `rfc4880` which applies encryption
as defined in [rfc4880](https://datatracker.ietf.org/doc/html/rfc4880).
To explore, which profiles are available, refer to the dedicated [section](#explore-profiles).
By default, the encrypted message will be ASCII armored. To disable ASCII armor, call `noArmor()` before the
`plaintext(_)` method call.
@ -269,7 +209,7 @@ byte[] ciphertext = ...; // the encrypted message
ReadyWithResult<DecryptionResult> readyWithResult = sop.decrypt()
.withKey(bobKey)
.verifyWithCert(aliceCert)
.verifyWith(aliceCert)
.withKeyPassword("password123") // if decryption key is protected
.ciphertext(ciphertext);
```
@ -524,23 +464,3 @@ By default, the signatures output will be ASCII armored. This can be disabled by
prior to `message(_)`.
The detached signatures can now be verified like in the section above.
### Explore Profiles
Certain operations allow modification of their behaviour by selecting between different profiles.
An example for this is the `generateKey()` operation, where different profiles result in different algorithms used
during key generation.
To explore, which profiles are supported by a certain operation, you can use the `listProfiles()` operation.
For example, this is how you can get a list of profiles supported by the `generateKey()` operation:
```java
List<Profile> profiles = sop.listProfiles().subcommand("generate-key");
```
:::{note}
As you can see, the argument passed into the `subcommand()` method must match the operation name as defined in the
[Stateless OpenPGP Protocol specification](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/).
:::
At the time of writing (the latest revision of the SOP spec is 06), only `generate-key` and `encrypt` accept profiles.

View file

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.4-rc-1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View file

@ -67,7 +67,6 @@ graph LR
enc -. "ε,ε/m" .-> pgpmsg
enc -- "ε,#/ε" --> accept
enc -- "Signature,o/ε" --> sig4ops
accept -- "ε,ε/ε" --> accept
```
Formally, the PDA is defined as $M = (\mathcal{Q}, \Sigma, \Upgamma, \delta, q_0, Z, F)$, where

View file

@ -4,13 +4,27 @@
plugins {
id 'application'
id 'org.graalvm.buildtools.native' version '0.10.6'
id 'com.gradleup.shadow' version '8.3.6'
id "com.github.johnrengelman.shadow" version "6.1.0"
}
def generatedVersionDir = "${buildDir}/generated-version"
sourceSets {
main {
output.dir(generatedVersionDir, builtBy: 'generateVersionProperties')
}
}
graalvmNative {
toolchainDetection = true
task generateVersionProperties {
doLast {
def propertiesFile = file "$generatedVersionDir/version.properties"
propertiesFile.parentFile.mkdirs()
propertiesFile.createNewFile()
// Instead of using a Properties object here, we directly write to the file
// since Properties adds a timestamp, ruining reproducibility
propertiesFile.write("version="+rootProject.version.toString())
}
}
processResources.dependsOn generateVersionProperties
dependencies {
@ -18,12 +32,13 @@ dependencies {
testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion"
// https://todd.ginsberg.com/post/testing-system-exit/
testImplementation 'com.ginsberg:junit5-system-exit:1.1.2'
// implementation "ch.qos.logback:logback-core:1.2.6"
// We want logback logging in tests and in the app
testImplementation "ch.qos.logback:logback-classic:$logbackVersion"
// implementation "ch.qos.logback:logback-classic:$logbackVersion"
implementation "org.slf4j:slf4j-nop:$slf4jVersion"
implementation "ch.qos.logback:logback-classic:$logbackVersion"
implementation(project(":pgpainless-sop"))
implementation "org.pgpainless:sop-java-picocli:$sopJavaVersion"
@ -37,6 +52,22 @@ mainClassName = 'org.pgpainless.cli.PGPainlessCLI'
application {
mainClass = mainClassName
}
/**
jar {
duplicatesStrategy(DuplicatesStrategy.EXCLUDE)
manifest {
attributes 'Main-Class': "$mainClassName"
}
from {
configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
} {
exclude "META-INF/*.SF"
exclude "META-INF/*.DSA"
exclude "META-INF/*.RSA"
}
}
*/
run {
// https://stackoverflow.com/questions/59445306/pipe-into-gradle-run
@ -46,3 +77,5 @@ run {
args Eval.me(appArgs)
}
}
// tasks."jar".dependsOn(":pgpainless-core:assemble", ":pgpainless-sop:assemble")

View file

@ -2,11 +2,12 @@
.\" Title: pgpainless-cli-armor
.\" Author: [see the "AUTHOR(S)" section]
.\" Generator: Asciidoctor 2.0.10
.\" Date: 2022-11-06
.\" Manual: PGPainless-CLI Manual
.\" Source:
.\" Language: English
.\"
.TH "PGPAINLESS\-CLI\-ARMOR" "1" "" "" "PGPainless\-CLI Manual"
.TH "PGPAINLESS\-CLI\-ARMOR" "1" "2022-11-06" "" "PGPainless\-CLI Manual"
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.ss \n[.ss] 0
@ -30,12 +31,17 @@
pgpainless\-cli\-armor \- Add ASCII Armor to standard input
.SH "SYNOPSIS"
.sp
\fBpgpainless\-cli armor\fP [\fB\-\-stacktrace\fP]
\fBpgpainless\-cli armor\fP [\fB\-\-stacktrace\fP] [\fB\-\-label\fP=\fI{auto|sig|key|cert|message}\fP]
.SH "DESCRIPTION"
.SH "OPTIONS"
.sp
\fB\-\-label\fP=\fI{auto|sig|key|cert|message}\fP
.RS 4
Label to be used in the header and tail of the armoring
.RE
.sp
\fB\-\-stacktrace\fP
.RS 4
Print stacktrace
Print Stacktrace
.RE

View file

@ -1,67 +0,0 @@
'\" t
.\" Title: pgpainless-cli-change-key-password
.\" Author: [see the "AUTHOR(S)" section]
.\" Generator: Asciidoctor 2.0.10
.\" Manual: PGPainless-CLI Manual
.\" Source:
.\" Language: English
.\"
.TH "PGPAINLESS\-CLI\-CHANGE\-KEY\-PASSWORD" "1" "" "" "PGPainless\-CLI Manual"
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.ss \n[.ss] 0
.nh
.ad l
.de URL
\fI\\$2\fP <\\$1>\\$3
..
.als MTO URL
.if \n[.g] \{\
. mso www.tmac
. am URL
. ad l
. .
. am MTO
. ad l
. .
. LINKSTYLE blue R < >
.\}
.SH "NAME"
pgpainless\-cli\-change\-key\-password \- Update the password of a key
.SH "SYNOPSIS"
.sp
\fBpgpainless\-cli change\-key\-password\fP [\fB\-\-[no\-]armor\fP] [\fB\-\-stacktrace\fP] [\fB\-\-new\-key\-password\fP
[=\fIPASSWORD\fP]] [\fB\-\-old\-key\-password\fP=\fIPASSWORD\fP]...
.SH "DESCRIPTION"
.sp
Unlock all secret keys from STDIN using the given old passwords and emit them re\-locked using the new password to STDOUT.
If any (sub\-) key cannot be unlocked, this operation will exit with error code 67.
.SH "OPTIONS"
.sp
\fB\-\-new\-key\-password\fP[=\fIPASSWORD\fP]
.RS 4
New password to lock the keys with.
.sp
If no new password is passed in, the keys will be emitted unlocked.
.sp
Is an INDIRECT data type (e.g. file, environment variable, file descriptor...).
.RE
.sp
\fB\-\-[no\-]armor\fP
.RS 4
ASCII armor the output
.RE
.sp
\fB\-\-old\-key\-password\fP=\fIPASSWORD\fP
.RS 4
Old passwords to unlock the keys with.
.sp
Multiple passwords can be passed in, which are tested sequentially to unlock locked subkeys.
.sp
Is an INDIRECT data type (e.g. file, environment variable, file descriptor...).
.RE
.sp
\fB\-\-stacktrace\fP
.RS 4
Print stacktrace
.RE

View file

@ -2,11 +2,12 @@
.\" Title: pgpainless-cli-dearmor
.\" Author: [see the "AUTHOR(S)" section]
.\" Generator: Asciidoctor 2.0.10
.\" Date: 2022-11-06
.\" Manual: PGPainless-CLI Manual
.\" Source:
.\" Language: English
.\"
.TH "PGPAINLESS\-CLI\-DEARMOR" "1" "" "" "PGPainless\-CLI Manual"
.TH "PGPAINLESS\-CLI\-DEARMOR" "1" "2022-11-06" "" "PGPainless\-CLI Manual"
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.ss \n[.ss] 0
@ -37,5 +38,5 @@ pgpainless\-cli\-dearmor \- Remove ASCII Armor from standard input
.sp
\fB\-\-stacktrace\fP
.RS 4
Print stacktrace
Print Stacktrace
.RE

View file

@ -2,11 +2,12 @@
.\" Title: pgpainless-cli-decrypt
.\" Author: [see the "AUTHOR(S)" section]
.\" Generator: Asciidoctor 2.0.10
.\" Date: 2022-11-06
.\" Manual: PGPainless-CLI Manual
.\" Source:
.\" Language: English
.\"
.TH "PGPAINLESS\-CLI\-DECRYPT" "1" "" "" "PGPainless\-CLI Manual"
.TH "PGPAINLESS\-CLI\-DECRYPT" "1" "2022-11-06" "" "PGPainless\-CLI Manual"
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.ss \n[.ss] 0
@ -27,29 +28,19 @@
. LINKSTYLE blue R < >
.\}
.SH "NAME"
pgpainless\-cli\-decrypt \- Decrypt a message
pgpainless\-cli\-decrypt \- Decrypt a message from standard input
.SH "SYNOPSIS"
.sp
\fBpgpainless\-cli decrypt\fP [\fB\-\-stacktrace\fP] [\fB\-\-session\-key\-out\fP=\fISESSIONKEY\fP]
[\fB\-\-verifications\-out\fP=\fIVERIFICATIONS\fP] [\fB\-\-verify\-not\-after\fP=\fIDATE\fP]
[\fB\-\-verify\-not\-before\fP=\fIDATE\fP] [\fB\-\-verify\-with\fP=\fICERT\fP]...
[\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP]... [\fB\-\-with\-password\fP=\fIPASSWORD\fP]...
[\fB\-\-with\-session\-key\fP=\fISESSIONKEY\fP]... [\fIKEY\fP...]
\fBpgpainless\-cli decrypt\fP [\fB\-\-stacktrace\fP] [\fB\-\-not\-after\fP=\fIDATE\fP] [\fB\-\-not\-before\fP=\fIDATE\fP]
[\fB\-\-session\-key\-out\fP=\fISESSIONKEY\fP] [\fB\-\-verify\-out\fP=\fIVERIFICATIONS\fP]
[\fB\-\-verify\-with\fP=\fICERT\fP]... [\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP]...
[\fB\-\-with\-password\fP=\fIPASSWORD\fP]... [\fB\-\-with\-session\-key\fP=\fISESSIONKEY\fP]...
[\fIKEY\fP...]
.SH "DESCRIPTION"
.SH "OPTIONS"
.sp
\fB\-\-session\-key\-out\fP=\fISESSIONKEY\fP
.RS 4
Can be used to learn the session key on successful decryption
.RE
.sp
\fB\-\-stacktrace\fP
.RS 4
Print stacktrace
.RE
.sp
\fB\-\-verifications\-out\fP=\fIVERIFICATIONS\fP, \fB\-\-verify\-not\-after\fP=\fIDATE\fP
\fB\-\-not\-after\fP=\fIDATE\fP
.RS 4
ISO\-8601 formatted UTC date (e.g. \(aq2020\-11\-23T16:35Z)
.sp
@ -60,7 +51,7 @@ Defaults to current system time (\(aqnow\(aq).
Accepts special value \(aq\-\(aq for end of time.
.RE
.sp
\fB\-\-verify\-not\-before\fP=\fIDATE\fP
\fB\-\-not\-before\fP=\fIDATE\fP
.RS 4
ISO\-8601 formatted UTC date (e.g. \(aq2020\-11\-23T16:35Z)
.sp
@ -69,6 +60,21 @@ Reject signatures with a creation date not in range.
Defaults to beginning of time (\(aq\-\(aq).
.RE
.sp
\fB\-\-session\-key\-out\fP=\fISESSIONKEY\fP
.RS 4
Can be used to learn the session key on successful decryption
.RE
.sp
\fB\-\-stacktrace\fP
.RS 4
Print Stacktrace
.RE
.sp
\fB\-\-verify\-out\fP=\fIVERIFICATIONS\fP
.RS 4
Emits signature verification status to the designated output
.RE
.sp
\fB\-\-verify\-with\fP=\fICERT\fP
.RS 4
Certificates for signature verification
@ -87,7 +93,7 @@ Symmetric passphrase to decrypt the message with.
.sp
Enables decryption based on any "SKESK" packets in the "CIPHERTEXT".
.sp
Is an INDIRECT data type (e.g. file, environment variable, file descriptor...).
Is an INDIRECT data type (e.g. file, environment variable, file descriptor...)
.RE
.sp
\fB\-\-with\-session\-key\fP=\fISESSIONKEY\fP
@ -96,7 +102,7 @@ Symmetric message key (session key).
.sp
Enables decryption of the "CIPHERTEXT" using the session key directly against the "SEIPD" packet.
.sp
Is an INDIRECT data type (e.g. file, environment variable, file descriptor...).
Is an INDIRECT data type (e.g. file, environment variable, file descriptor...)
.RE
.SH "ARGUMENTS"
.sp

View file

@ -2,11 +2,12 @@
.\" Title: pgpainless-cli-encrypt
.\" Author: [see the "AUTHOR(S)" section]
.\" Generator: Asciidoctor 2.0.10
.\" Date: 2022-11-06
.\" Manual: PGPainless-CLI Manual
.\" Source:
.\" Language: English
.\"
.TH "PGPAINLESS\-CLI\-ENCRYPT" "1" "" "" "PGPainless\-CLI Manual"
.TH "PGPAINLESS\-CLI\-ENCRYPT" "1" "2022-11-06" "" "PGPainless\-CLI Manual"
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.ss \n[.ss] 0
@ -31,7 +32,6 @@ pgpainless\-cli\-encrypt \- Encrypt a message from standard input
.SH "SYNOPSIS"
.sp
\fBpgpainless\-cli encrypt\fP [\fB\-\-[no\-]armor\fP] [\fB\-\-stacktrace\fP] [\fB\-\-as\fP=\fI{binary|text}\fP]
[\fB\-\-profile\fP=\fIPROFILE\fP] [\fB\-\-session\-key\-out\fP=\fISESSIONKEY\fP]
[\fB\-\-sign\-with\fP=\fIKEY\fP]... [\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP]...
[\fB\-\-with\-password\fP=\fIPASSWORD\fP]... [\fICERTS\fP...]
.SH "DESCRIPTION"
@ -48,19 +48,14 @@ Type of the input data. Defaults to \(aqbinary\(aq
ASCII armor the output
.RE
.sp
\fB\-\-profile\fP=\fIPROFILE\fP
.RS 4
Profile identifier to switch between profiles
.RE
.sp
\fB\-\-session\-key\-out\fP=\fISESSIONKEY\fP, \fB\-\-sign\-with\fP=\fIKEY\fP
\fB\-\-sign\-with\fP=\fIKEY\fP
.RS 4
Sign the output with a private key
.RE
.sp
\fB\-\-stacktrace\fP
.RS 4
Print stacktrace
Print Stacktrace
.RE
.sp
\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP
@ -74,7 +69,7 @@ Is an INDIRECT data type (e.g. file, environment variable, file descriptor...).
.RS 4
Encrypt the message with a password.
.sp
Is an INDIRECT data type (e.g. file, environment variable, file descriptor...).
Is an INDIRECT data type (e.g. file, environment variable, file descriptor...)
.RE
.SH "ARGUMENTS"
.sp

View file

@ -2,11 +2,12 @@
.\" Title: pgpainless-cli-extract-cert
.\" Author: [see the "AUTHOR(S)" section]
.\" Generator: Asciidoctor 2.0.10
.\" Date: 2022-11-06
.\" Manual: PGPainless-CLI Manual
.\" Source:
.\" Language: English
.\"
.TH "PGPAINLESS\-CLI\-EXTRACT\-CERT" "1" "" "" "PGPainless\-CLI Manual"
.TH "PGPAINLESS\-CLI\-EXTRACT\-CERT" "1" "2022-11-06" "" "PGPainless\-CLI Manual"
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.ss \n[.ss] 0
@ -27,13 +28,12 @@
. LINKSTYLE blue R < >
.\}
.SH "NAME"
pgpainless\-cli\-extract\-cert \- Extract a public key certificate from a secret key
pgpainless\-cli\-extract\-cert \- Extract a public key certificate from a secret key from standard input
.SH "SYNOPSIS"
.sp
\fBpgpainless\-cli extract\-cert\fP [\fB\-\-[no\-]armor\fP] [\fB\-\-stacktrace\fP]
.SH "DESCRIPTION"
.sp
Read a secret key from STDIN and emit the public key certificate to STDOUT.
.SH "OPTIONS"
.sp
\fB\-\-[no\-]armor\fP
@ -43,5 +43,5 @@ ASCII armor the output
.sp
\fB\-\-stacktrace\fP
.RS 4
Print stacktrace
Print Stacktrace
.RE

View file

@ -2,11 +2,12 @@
.\" Title: pgpainless-cli-generate-completion
.\" Author: [see the "AUTHOR(S)" section]
.\" Generator: Asciidoctor 2.0.10
.\" Date: 2022-11-06
.\" Manual: PGPainless-CLI Manual
.\" Source: generate-completion 4.6.3
.\" Language: English
.\"
.TH "PGPAINLESS\-CLI\-GENERATE\-COMPLETION" "1" "" "generate\-completion 4.6.3" "PGPainless\-CLI Manual"
.TH "PGPAINLESS\-CLI\-GENERATE\-COMPLETION" "1" "2022-11-06" "generate\-completion 4.6.3" "PGPainless\-CLI Manual"
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.ss \n[.ss] 0
@ -50,7 +51,7 @@ Show this help message and exit.
.sp
\fB\-\-stacktrace\fP
.RS 4
Print stacktrace
Print Stacktrace
.RE
.sp
\fB\-V\fP, \fB\-\-version\fP
@ -141,7 +142,7 @@ Unsupported subcommand
.sp
\fB71\fP
.RS 4
Unsupported special prefix (e.g. "@ENV/@FD") of indirect parameter
Unsupported special prefix (e.g. "@env/@fd") of indirect parameter
.RE
.sp
\fB73\fP
@ -152,14 +153,4 @@ Ambiguous input (a filename matching the designator already exists)
\fB79\fP
.RS 4
Key is not signing capable
.RE
.sp
\fB83\fP
.RS 4
Options were supplied that are incompatible with each other
.RE
.sp
\fB89\fP
.RS 4
The requested profile is unsupported, or the indicated subcommand does not accept profiles
.RE

View file

@ -2,11 +2,12 @@
.\" Title: pgpainless-cli-generate-key
.\" Author: [see the "AUTHOR(S)" section]
.\" Generator: Asciidoctor 2.0.10
.\" Date: 2022-11-06
.\" Manual: PGPainless-CLI Manual
.\" Source:
.\" Language: English
.\"
.TH "PGPAINLESS\-CLI\-GENERATE\-KEY" "1" "" "" "PGPainless\-CLI Manual"
.TH "PGPAINLESS\-CLI\-GENERATE\-KEY" "1" "2022-11-06" "" "PGPainless\-CLI Manual"
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.ss \n[.ss] 0
@ -30,8 +31,8 @@
pgpainless\-cli\-generate\-key \- Generate a secret key
.SH "SYNOPSIS"
.sp
\fBpgpainless\-cli generate\-key\fP [\fB\-\-[no\-]armor\fP] [\fB\-\-signing\-only\fP] [\fB\-\-stacktrace\fP]
[\fB\-\-profile\fP=\fIPROFILE\fP] [\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP] [\fIUSERID\fP...]
\fBpgpainless\-cli generate\-key\fP [\fB\-\-[no\-]armor\fP] [\fB\-\-stacktrace\fP] [\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP]
[\fIUSERID\fP...]
.SH "DESCRIPTION"
.SH "OPTIONS"
@ -41,19 +42,9 @@ pgpainless\-cli\-generate\-key \- Generate a secret key
ASCII armor the output
.RE
.sp
\fB\-\-profile\fP=\fIPROFILE\fP
.RS 4
Profile identifier to switch between profiles
.RE
.sp
\fB\-\-signing\-only\fP
.RS 4
Generate a key that can only be used for signing
.RE
.sp
\fB\-\-stacktrace\fP
.RS 4
Print stacktrace
Print Stacktrace
.RE
.sp
\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP

View file

@ -2,11 +2,12 @@
.\" Title: pgpainless-cli-help
.\" Author: [see the "AUTHOR(S)" section]
.\" Generator: Asciidoctor 2.0.10
.\" Date: 2022-11-06
.\" Manual: PGPainless-CLI Manual
.\" Source:
.\" Language: English
.\"
.TH "PGPAINLESS\-CLI\-HELP" "1" "" "" "PGPainless\-CLI Manual"
.TH "PGPAINLESS\-CLI\-HELP" "1" "2022-11-06" "" "PGPainless\-CLI Manual"
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.ss \n[.ss] 0
@ -44,7 +45,7 @@ Show usage help for the help command and exit.
.sp
\fB\-\-stacktrace\fP
.RS 4
Print stacktrace
Print Stacktrace
.RE
.SH "ARGUMENTS"
.sp
@ -136,7 +137,7 @@ Unsupported subcommand
.sp
\fB71\fP
.RS 4
Unsupported special prefix (e.g. "@ENV/@FD") of indirect parameter
Unsupported special prefix (e.g. "@env/@fd") of indirect parameter
.RE
.sp
\fB73\fP
@ -147,14 +148,4 @@ Ambiguous input (a filename matching the designator already exists)
\fB79\fP
.RS 4
Key is not signing capable
.RE
.sp
\fB83\fP
.RS 4
Options were supplied that are incompatible with each other
.RE
.sp
\fB89\fP
.RS 4
The requested profile is unsupported, or the indicated subcommand does not accept profiles
.RE

View file

@ -2,11 +2,12 @@
.\" Title: pgpainless-cli-inline-detach
.\" Author: [see the "AUTHOR(S)" section]
.\" Generator: Asciidoctor 2.0.10
.\" Date: 2022-11-06
.\" Manual: PGPainless-CLI Manual
.\" Source:
.\" Language: English
.\"
.TH "PGPAINLESS\-CLI\-INLINE\-DETACH" "1" "" "" "PGPainless\-CLI Manual"
.TH "PGPAINLESS\-CLI\-INLINE\-DETACH" "1" "2022-11-06" "" "PGPainless\-CLI Manual"
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.ss \n[.ss] 0
@ -47,5 +48,5 @@ Destination to which a detached signatures block will be written
.sp
\fB\-\-stacktrace\fP
.RS 4
Print stacktrace
Print Stacktrace
.RE

View file

@ -2,11 +2,12 @@
.\" Title: pgpainless-cli-inline-sign
.\" Author: [see the "AUTHOR(S)" section]
.\" Generator: Asciidoctor 2.0.10
.\" Date: 2022-11-06
.\" Manual: PGPainless-CLI Manual
.\" Source:
.\" Language: English
.\"
.TH "PGPAINLESS\-CLI\-INLINE\-SIGN" "1" "" "" "PGPainless\-CLI Manual"
.TH "PGPAINLESS\-CLI\-INLINE\-SIGN" "1" "2022-11-06" "" "PGPainless\-CLI Manual"
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.ss \n[.ss] 0
@ -27,22 +28,23 @@
. LINKSTYLE blue R < >
.\}
.SH "NAME"
pgpainless\-cli\-inline\-sign \- Create an inline\-signed message
pgpainless\-cli\-inline\-sign \- Create an inline\-signed message from data on standard input
.SH "SYNOPSIS"
.sp
\fBpgpainless\-cli inline\-sign\fP [\fB\-\-[no\-]armor\fP] [\fB\-\-stacktrace\fP] [\fB\-\-as\fP=\fI{binary|text|clearsigned}\fP]
\fBpgpainless\-cli inline\-sign\fP [\fB\-\-[no\-]armor\fP] [\fB\-\-stacktrace\fP] [\fB\-\-as\fP=
\fI{binary|text|cleartextsigned}\fP]
[\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP]... [\fIKEYS\fP...]
.SH "DESCRIPTION"
.SH "OPTIONS"
.sp
\fB\-\-as\fP=\fI{binary|text|clearsigned}\fP
\fB\-\-as\fP=\fI{binary|text|cleartextsigned}\fP
.RS 4
Specify the signature format of the signed message.
Specify the signature format of the signed message
.sp
\(aqtext\(aq and \(aqbinary\(aq will produce inline\-signed messages.
.sp
\(aqclearsigned\(aq will make use of the cleartext signature framework.
\(aqcleartextsigned\(aq will make use of the cleartext signature framework.
.sp
Defaults to \(aqbinary\(aq.
.sp
@ -56,7 +58,7 @@ ASCII armor the output
.sp
\fB\-\-stacktrace\fP
.RS 4
Print stacktrace
Print Stacktrace
.RE
.sp
\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP

View file

@ -2,11 +2,12 @@
.\" Title: pgpainless-cli-inline-verify
.\" Author: [see the "AUTHOR(S)" section]
.\" Generator: Asciidoctor 2.0.10
.\" Date: 2022-11-06
.\" Manual: PGPainless-CLI Manual
.\" Source:
.\" Language: English
.\"
.TH "PGPAINLESS\-CLI\-INLINE\-VERIFY" "1" "" "" "PGPainless\-CLI Manual"
.TH "PGPAINLESS\-CLI\-INLINE\-VERIFY" "1" "2022-11-06" "" "PGPainless\-CLI Manual"
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.ss \n[.ss] 0
@ -27,11 +28,11 @@
. LINKSTYLE blue R < >
.\}
.SH "NAME"
pgpainless\-cli\-inline\-verify \- Verify an inline\-signed message
pgpainless\-cli\-inline\-verify \- Verify inline\-signed data from standard input
.SH "SYNOPSIS"
.sp
\fBpgpainless\-cli inline\-verify\fP [\fB\-\-stacktrace\fP] [\fB\-\-not\-after\fP=\fIDATE\fP] [\fB\-\-not\-before\fP=\fIDATE\fP]
[\fB\-\-verifications\-out\fP=\fIVERIFICATIONS\fP] [\fICERT\fP...]
[\fB\-\-verifications\-out\fP=\fI<verificationsOut>\fP] [\fICERT\fP...]
.SH "DESCRIPTION"
.SH "OPTIONS"
@ -58,10 +59,10 @@ Defaults to beginning of time ("\-").
.sp
\fB\-\-stacktrace\fP
.RS 4
Print stacktrace
Print Stacktrace
.RE
.sp
\fB\-\-verifications\-out\fP=\fIVERIFICATIONS\fP
\fB\-\-verifications\-out\fP=\fI<verificationsOut>\fP
.RS 4
File to write details over successful verifications to
.RE

View file

@ -1,47 +0,0 @@
'\" t
.\" Title: pgpainless-cli-list-profiles
.\" Author: [see the "AUTHOR(S)" section]
.\" Generator: Asciidoctor 2.0.10
.\" Manual: PGPainless-CLI Manual
.\" Source:
.\" Language: English
.\"
.TH "PGPAINLESS\-CLI\-LIST\-PROFILES" "1" "" "" "PGPainless\-CLI Manual"
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.ss \n[.ss] 0
.nh
.ad l
.de URL
\fI\\$2\fP <\\$1>\\$3
..
.als MTO URL
.if \n[.g] \{\
. mso www.tmac
. am URL
. ad l
. .
. am MTO
. ad l
. .
. LINKSTYLE blue R < >
.\}
.SH "NAME"
pgpainless\-cli\-list\-profiles \- Emit a list of profiles supported by the identified subcommand
.SH "SYNOPSIS"
.sp
\fBpgpainless\-cli list\-profiles\fP [\fB\-\-stacktrace\fP] \fICOMMAND\fP
.SH "DESCRIPTION"
.SH "OPTIONS"
.sp
\fB\-\-stacktrace\fP
.RS 4
Print stacktrace
.RE
.SH "ARGUMENTS"
.sp
\fICOMMAND\fP
.RS 4
Subcommand for which to list profiles
.RE

View file

@ -1,54 +0,0 @@
'\" t
.\" Title: pgpainless-cli-revoke-key
.\" Author: [see the "AUTHOR(S)" section]
.\" Generator: Asciidoctor 2.0.10
.\" Manual: PGPainless-CLI Manual
.\" Source:
.\" Language: English
.\"
.TH "PGPAINLESS\-CLI\-REVOKE\-KEY" "1" "" "" "PGPainless\-CLI Manual"
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.ss \n[.ss] 0
.nh
.ad l
.de URL
\fI\\$2\fP <\\$1>\\$3
..
.als MTO URL
.if \n[.g] \{\
. mso www.tmac
. am URL
. ad l
. .
. am MTO
. ad l
. .
. LINKSTYLE blue R < >
.\}
.SH "NAME"
pgpainless\-cli\-revoke\-key \- Generate revocation certificates
.SH "SYNOPSIS"
.sp
\fBpgpainless\-cli revoke\-key\fP [\fB\-\-[no\-]armor\fP] [\fB\-\-stacktrace\fP] [\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP]
.SH "DESCRIPTION"
.sp
Emit revocation certificates for secret keys from STDIN to STDOUT.
.SH "OPTIONS"
.sp
\fB\-\-[no\-]armor\fP
.RS 4
ASCII armor the output
.RE
.sp
\fB\-\-stacktrace\fP
.RS 4
Print stacktrace
.RE
.sp
\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP
.RS 4
Passphrase to unlock the secret key(s).
.sp
Is an INDIRECT data type (e.g. file, environment variable, file descriptor...).
.RE

View file

@ -2,11 +2,12 @@
.\" Title: pgpainless-cli-sign
.\" Author: [see the "AUTHOR(S)" section]
.\" Generator: Asciidoctor 2.0.10
.\" Date: 2022-11-06
.\" Manual: PGPainless-CLI Manual
.\" Source:
.\" Language: English
.\"
.TH "PGPAINLESS\-CLI\-SIGN" "1" "" "" "PGPainless\-CLI Manual"
.TH "PGPAINLESS\-CLI\-SIGN" "1" "2022-11-06" "" "PGPainless\-CLI Manual"
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.ss \n[.ss] 0
@ -27,7 +28,7 @@
. LINKSTYLE blue R < >
.\}
.SH "NAME"
pgpainless\-cli\-sign \- Create a detached message signature
pgpainless\-cli\-sign \- Create a detached signature on the data from standard input
.SH "SYNOPSIS"
.sp
\fBpgpainless\-cli sign\fP [\fB\-\-[no\-]armor\fP] [\fB\-\-stacktrace\fP] [\fB\-\-as\fP=\fI{binary|text}\fP]
@ -38,7 +39,7 @@ pgpainless\-cli\-sign \- Create a detached message signature
.sp
\fB\-\-as\fP=\fI{binary|text}\fP
.RS 4
Specify the output format of the signed message.
Specify the output format of the signed message
.sp
Defaults to \(aqbinary\(aq.
.sp
@ -47,7 +48,7 @@ If \(aq\-\-as=text\(aq and the input data is not valid UTF\-8, sign fails with r
.sp
\fB\-\-micalg\-out\fP=\fIMICALG\fP
.RS 4
Emits the digest algorithm used to the specified file in a way that can be used to populate the micalg parameter for the PGP/MIME Content\-Type (RFC3156).
Emits the digest algorithm used to the specified file in a way that can be used to populate the micalg parameter for the PGP/MIME Content\-Type (RFC3156)
.RE
.sp
\fB\-\-[no\-]armor\fP
@ -57,7 +58,7 @@ ASCII armor the output
.sp
\fB\-\-stacktrace\fP
.RS 4
Print stacktrace
Print Stacktrace
.RE
.sp
\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP

View file

@ -2,11 +2,12 @@
.\" Title: pgpainless-cli-verify
.\" Author: [see the "AUTHOR(S)" section]
.\" Generator: Asciidoctor 2.0.10
.\" Date: 2022-11-06
.\" Manual: PGPainless-CLI Manual
.\" Source:
.\" Language: English
.\"
.TH "PGPAINLESS\-CLI\-VERIFY" "1" "" "" "PGPainless\-CLI Manual"
.TH "PGPAINLESS\-CLI\-VERIFY" "1" "2022-11-06" "" "PGPainless\-CLI Manual"
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.ss \n[.ss] 0
@ -27,28 +28,16 @@
. LINKSTYLE blue R < >
.\}
.SH "NAME"
pgpainless\-cli\-verify \- Verify a detached signature
pgpainless\-cli\-verify \- Verify a detached signature over the data from standard input
.SH "SYNOPSIS"
.sp
\fBpgpainless\-cli verify\fP [\fB\-\-stacktrace\fP] [\fB\-\-not\-after\fP=\fIDATE\fP] [\fB\-\-not\-before\fP=\fIDATE\fP] \fISIGNATURE\fP
\fICERT\fP...
.SH "DESCRIPTION"
.sp
Verify a detached signature over some data from STDIN.
.SH "OPTIONS"
.sp
\fB\-\-not\-after\fP=\fIDATE\fP
.RS 4
ISO\-8601 formatted UTC date (e.g. \(aq2020\-11\-23T16:35Z)
.sp
Reject signatures with a creation date not in range.
.sp
Defaults to current system time ("now").
.sp
Accepts special value "\-" for end of time.
.RE
.sp
\fB\-\-not\-before\fP=\fIDATE\fP
\fB\-\-not\-after\fP=\fIDATE\fP, \fB\-\-not\-before\fP=\fIDATE\fP
.RS 4
ISO\-8601 formatted UTC date (e.g. \(aq2020\-11\-23T16:35Z)
.sp
@ -59,7 +48,7 @@ Defaults to beginning of time ("\-").
.sp
\fB\-\-stacktrace\fP
.RS 4
Print stacktrace
Print Stacktrace
.RE
.SH "ARGUMENTS"
.sp
@ -70,5 +59,4 @@ Detached signature
.sp
\fICERT\fP...
.RS 4
Public key certificates for signature verification
.RE

View file

@ -2,11 +2,12 @@
.\" Title: pgpainless-cli-version
.\" Author: [see the "AUTHOR(S)" section]
.\" Generator: Asciidoctor 2.0.10
.\" Date: 2022-11-06
.\" Manual: PGPainless-CLI Manual
.\" Source:
.\" Language: English
.\"
.TH "PGPAINLESS\-CLI\-VERSION" "1" "" "" "PGPainless\-CLI Manual"
.TH "PGPAINLESS\-CLI\-VERSION" "1" "2022-11-06" "" "PGPainless\-CLI Manual"
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.ss \n[.ss] 0
@ -30,7 +31,7 @@
pgpainless\-cli\-version \- Display version information about the tool
.SH "SYNOPSIS"
.sp
\fBpgpainless\-cli version\fP [\fB\-\-stacktrace\fP] [\fB\-\-extended\fP | \fB\-\-backend\fP | \fB\-\-pgpainless\-cli\-spec\fP | \fB\-\-sopv\fP]
\fBpgpainless\-cli version\fP [\fB\-\-stacktrace\fP] [\fB\-\-extended\fP | \fB\-\-backend\fP]
.SH "DESCRIPTION"
.SH "OPTIONS"
@ -45,12 +46,7 @@ Print information about the cryptographic backend
Print an extended version string
.RE
.sp
\fB\-\-pgpainless\-cli\-spec\fP
\fB\-\-stacktrace\fP
.RS 4
Print the latest revision of the SOP specification targeted by the implementation
.RE
.sp
\fB\-\-sopv\fP, \fB\-\-stacktrace\fP
.RS 4
Print stacktrace
Print Stacktrace
.RE

View file

@ -2,11 +2,12 @@
.\" Title: pgpainless-cli
.\" Author: [see the "AUTHOR(S)" section]
.\" Generator: Asciidoctor 2.0.10
.\" Date: 2022-11-06
.\" Manual: PGPainless-CLI Manual
.\" Source:
.\" Language: English
.\"
.TH "PGPAINLESS\-CLI" "1" "" "" "PGPainless\-CLI Manual"
.TH "PGPAINLESS\-CLI" "1" "2022-11-06" "" "PGPainless\-CLI Manual"
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.ss \n[.ss] 0
@ -37,73 +38,13 @@ pgpainless\-cli \- Stateless OpenPGP Protocol
.sp
\fB\-\-stacktrace\fP
.RS 4
Print stacktrace
Print Stacktrace
.RE
.SH "COMMANDS"
.sp
\fBversion\fP
\fBhelp\fP
.RS 4
Display version information about the tool
.RE
.sp
\fBlist\-profiles\fP
.RS 4
Emit a list of profiles supported by the identified subcommand
.RE
.sp
\fBgenerate\-key\fP
.RS 4
Generate a secret key
.RE
.sp
\fBchange\-key\-password\fP
.RS 4
Update the password of a key
.RE
.sp
\fBrevoke\-key\fP
.RS 4
Generate revocation certificates
.RE
.sp
\fBextract\-cert\fP
.RS 4
Extract a public key certificate from a secret key
.RE
.sp
\fBsign\fP
.RS 4
Create a detached message signature
.RE
.sp
\fBverify\fP
.RS 4
Verify a detached signature
.RE
.sp
\fBencrypt\fP
.RS 4
Encrypt a message from standard input
.RE
.sp
\fBdecrypt\fP
.RS 4
Decrypt a message
.RE
.sp
\fBinline\-detach\fP
.RS 4
Split signatures from a clearsigned message
.RE
.sp
\fBinline\-sign\fP
.RS 4
Create an inline\-signed message
.RE
.sp
\fBinline\-verify\fP
.RS 4
Verify an inline\-signed message
Stateless OpenPGP Protocol
.RE
.sp
\fBarmor\fP
@ -116,9 +57,54 @@ Add ASCII Armor to standard input
Remove ASCII Armor from standard input
.RE
.sp
\fBhelp\fP
\fBdecrypt\fP
.RS 4
Stateless OpenPGP Protocol
Decrypt a message from standard input
.RE
.sp
\fBinline\-detach\fP
.RS 4
Split signatures from a clearsigned message
.RE
.sp
\fBencrypt\fP
.RS 4
Encrypt a message from standard input
.RE
.sp
\fBextract\-cert\fP
.RS 4
Extract a public key certificate from a secret key from standard input
.RE
.sp
\fBgenerate\-key\fP
.RS 4
Generate a secret key
.RE
.sp
\fBsign\fP
.RS 4
Create a detached signature on the data from standard input
.RE
.sp
\fBverify\fP
.RS 4
Verify a detached signature over the data from standard input
.RE
.sp
\fBinline\-sign\fP
.RS 4
Create an inline\-signed message from data on standard input
.RE
.sp
\fBinline\-verify\fP
.RS 4
Verify inline\-signed data from standard input
.RE
.sp
\fBversion\fP
.RS 4
Display version information about the tool
.RE
.sp
\fBgenerate\-completion\fP
@ -209,7 +195,7 @@ Unsupported subcommand
.sp
\fB71\fP
.RS 4
Unsupported special prefix (e.g. "@ENV/@FD") of indirect parameter
Unsupported special prefix (e.g. "@env/@fd") of indirect parameter
.RE
.sp
\fB73\fP
@ -220,14 +206,4 @@ Ambiguous input (a filename matching the designator already exists)
\fB79\fP
.RS 4
Key is not signing capable
.RE
.sp
\fB83\fP
.RS 4
Options were supplied that are incompatible with each other
.RE
.sp
\fB89\fP
.RS 4
The requested profile is unsupported, or the indicated subcommand does not accept profiles
.RE

View file

@ -4,7 +4,7 @@ SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
SOP_DIR=$(realpath $SCRIPT_DIR/../../sop-java)
[ ! -d "$SOP_DIR" ] && echo "sop-java repository MUST be cloned next to pgpainless repo" && exit 1;
SRC_DIR=$SOP_DIR/sop-java-picocli/build/docs/manpage
[ ! -d "$SRC_DIR" ] && echo "No sop manpages found. Please run 'gradle asciidoctor' in the sop-java repo." && exit 1;
[ ! -d "$SRC_DIR" ] && echo "No sop manpages found." && exit 1;
DEST_DIR=$SCRIPT_DIR/packaging/man
mkdir -p $DEST_DIR
@ -13,14 +13,12 @@ do
SRC="${page##*/}"
DEST="${SRC/sop/pgpainless-cli}"
sed \
-e 's/sopv/PLACEHOLDERV/g' \
-e 's#.\\" Title: sop#.\\" Title: pgpainless-cli#g' \
-e 's/Manual: Sop Manual/Manual: PGPainless-CLI Manual/g' \
-e 's/.TH "SOP/.TH "PGPAINLESS\\-CLI/g' \
-e 's/"Sop Manual"/"PGPainless\\-CLI Manual"/g' \
-e 's/\\fBsop/\\fBpgpainless\\-cli/g' \
-e 's/sop/pgpainless\\-cli/g' \
-e 's/PLACEHOLDERV/sopv/g' \
$page > $DEST_DIR/$DEST
done

View file

@ -14,18 +14,10 @@ import sop.cli.picocli.SopCLI;
public class PGPainlessCLI {
static {
// Prevent slf4j initialization logging
// https://github.com/qos-ch/slf4j/issues/422#issuecomment-2277280185
System.setProperty("slf4j.internal.verbosity", "WARN");
SopCLI.EXECUTABLE_NAME = "pgpainless-cli";
SopCLI.setSopInstance(new SOPImpl());
}
/**
* Main method of the CLI application.
* @param args arguments
*/
public static void main(String[] args) {
int result = execute(args);
if (result != 0) {
@ -33,12 +25,6 @@ public class PGPainlessCLI {
}
}
/**
* Execute the given command and return the exit code of the program.
*
* @param args command string array (e.g. ["pgpainless-cli", "generate-key", "Alice"])
* @return exit code
*/
public static int execute(String... args) {
return SopCLI.execute(args);
}

View file

@ -1,7 +0,0 @@
[
{
"type":"agent-extracted",
"classes":[
]
}
]

View file

@ -1,891 +0,0 @@
[
{
"name":"[Ljava.lang.Object;"
},
{
"name":"ch.qos.logback.classic.encoder.PatternLayoutEncoder",
"queryAllPublicMethods":true,
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"ch.qos.logback.classic.joran.SerializedModelConfigurator",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"ch.qos.logback.classic.util.DefaultJoranConfigurator",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"ch.qos.logback.core.ConsoleAppender",
"queryAllPublicMethods":true,
"methods":[{"name":"<init>","parameterTypes":[] }, {"name":"setTarget","parameterTypes":["java.lang.String"] }]
},
{
"name":"ch.qos.logback.core.OutputStreamAppender",
"methods":[{"name":"setEncoder","parameterTypes":["ch.qos.logback.core.encoder.Encoder"] }]
},
{
"name":"ch.qos.logback.core.encoder.Encoder",
"methods":[{"name":"valueOf","parameterTypes":["java.lang.String"] }]
},
{
"name":"ch.qos.logback.core.encoder.LayoutWrappingEncoder",
"methods":[{"name":"setParent","parameterTypes":["ch.qos.logback.core.spi.ContextAware"] }]
},
{
"name":"ch.qos.logback.core.pattern.PatternLayoutEncoderBase",
"methods":[{"name":"setPattern","parameterTypes":["java.lang.String"] }]
},
{
"name":"ch.qos.logback.core.spi.ContextAware",
"methods":[{"name":"valueOf","parameterTypes":["java.lang.String"] }]
},
{
"name":"com.sun.org.apache.xerces.internal.jaxp.SAXParserFactoryImpl",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"groovy.lang.Closure"
},
{
"name":"java.io.FilePermission"
},
{
"name":"java.lang.Enum"
},
{
"name":"java.lang.Object",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true
},
{
"name":"java.lang.RuntimePermission"
},
{
"name":"java.lang.System",
"methods":[{"name":"console","parameterTypes":[] }]
},
{
"name":"java.lang.invoke.MethodHandle"
},
{
"name":"java.net.NetPermission"
},
{
"name":"java.net.SocketPermission"
},
{
"name":"java.net.URLPermission",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String","java.lang.String"] }]
},
{
"name":"java.nio.channels.SelectionKey",
"fields":[{"name":"attachment"}]
},
{
"name":"java.nio.file.Path"
},
{
"name":"java.nio.file.Paths",
"methods":[{"name":"get","parameterTypes":["java.lang.String","java.lang.String[]"] }]
},
{
"name":"java.security.AllPermission"
},
{
"name":"java.security.MessageDigestSpi"
},
{
"name":"java.security.SecureRandomParameters"
},
{
"name":"java.security.SecurityPermission"
},
{
"name":"java.security.cert.PKIXRevocationChecker"
},
{
"name":"java.sql.Connection"
},
{
"name":"java.sql.Driver"
},
{
"name":"java.sql.DriverManager",
"methods":[{"name":"getConnection","parameterTypes":["java.lang.String"] }, {"name":"getDriver","parameterTypes":["java.lang.String"] }]
},
{
"name":"java.sql.Time",
"methods":[{"name":"<init>","parameterTypes":["long"] }]
},
{
"name":"java.sql.Timestamp",
"methods":[{"name":"valueOf","parameterTypes":["java.lang.String"] }]
},
{
"name":"java.time.Duration",
"methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }]
},
{
"name":"java.time.Instant",
"methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }]
},
{
"name":"java.time.LocalDate",
"methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }]
},
{
"name":"java.time.LocalDateTime",
"methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }]
},
{
"name":"java.time.LocalTime",
"methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }]
},
{
"name":"java.time.MonthDay",
"methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }]
},
{
"name":"java.time.OffsetDateTime",
"methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }]
},
{
"name":"java.time.OffsetTime",
"methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }]
},
{
"name":"java.time.Period",
"methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }]
},
{
"name":"java.time.Year",
"methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }]
},
{
"name":"java.time.YearMonth",
"methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }]
},
{
"name":"java.time.ZoneId",
"methods":[{"name":"of","parameterTypes":["java.lang.String"] }]
},
{
"name":"java.time.ZoneOffset",
"methods":[{"name":"of","parameterTypes":["java.lang.String"] }]
},
{
"name":"java.time.ZonedDateTime",
"methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }]
},
{
"name":"java.util.HashSet"
},
{
"name":"java.util.LinkedHashSet"
},
{
"name":"java.util.PropertyPermission"
},
{
"name":"java.util.concurrent.ArrayBlockingQueue"
},
{
"name":"java.util.concurrent.atomic.AtomicReference",
"fields":[{"name":"value"}]
},
{
"name":"java.util.concurrent.locks.AbstractOwnableSynchronizer"
},
{
"name":"java.util.concurrent.locks.AbstractQueuedSynchronizer"
},
{
"name":"java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject"
},
{
"name":"java.util.concurrent.locks.ReentrantLock"
},
{
"name":"java.util.concurrent.locks.ReentrantLock$NonfairSync"
},
{
"name":"java.util.concurrent.locks.ReentrantLock$Sync"
},
{
"name":"javax.smartcardio.CardPermission"
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.COMPOSITE$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.CONTEXT$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.CompositeSignatures$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.DH$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.DSA$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.DSTU4145$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.Dilithium$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.EC$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.ECGOST$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.EXTERNAL$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.EdEC$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.ElGamal$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.Falcon$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.GM$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.GOST$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.IES$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.LMS$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.MLDSA$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.MLKEM$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.NTRU$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.NoSig$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.RSA$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.SLHDSA$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.SPHINCSPlus$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.X509$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.edec.KeyPairGeneratorSpi$EdDSA",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.edec.KeyPairGeneratorSpi$XDH",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.rsa.KeyPairGeneratorSpi",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.digest.Blake2b$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.digest.Blake2s$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.digest.Blake3$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.digest.DSTU7564$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.digest.GOST3411$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.digest.Haraka$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.digest.Keccak$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.digest.MD2$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.digest.MD4$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.digest.MD5$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.digest.RIPEMD128$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.digest.RIPEMD160$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.digest.RIPEMD256$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.digest.RIPEMD320$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.digest.SHA1$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.digest.SHA224$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.digest.SHA256$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.digest.SHA3$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.digest.SHA384$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.digest.SHA512$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.digest.SM3$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.digest.Skein$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.digest.Tiger$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.digest.Whirlpool$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.drbg.DRBG$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.keystore.BC$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.keystore.BCFKS$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.keystore.PKCS12$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.AES$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.ARC4$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.ARIA$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.Blowfish$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.CAST5$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.CAST6$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.Camellia$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.ChaCha$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.DES$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.DESede$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.DSTU7624$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.GOST28147$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.GOST3412_2015$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.Grain128$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.Grainv1$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.HC128$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.HC256$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.IDEA$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.Noekeon$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.OpenSSLPBKDF$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.PBEPBKDF1$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.PBEPBKDF2$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.PBEPKCS12$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.Poly1305$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.RC2$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.RC5$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.RC6$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.Rijndael$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.SCRYPT$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.SEED$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.SM4$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.Salsa20$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.Serpent$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.Shacal2$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.SipHash$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.SipHash128$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.Skipjack$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.TEA$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.TLSKDF$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.Threefish$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.Twofish$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.VMPC$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.VMPCKSA3$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.XSalsa20$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.XTEA$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.Zuc$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.pgpainless.cli.ExitCodeTest",
"allDeclaredFields":true,
"allDeclaredClasses":true,
"queryAllDeclaredMethods":true,
"queryAllPublicMethods":true,
"queryAllDeclaredConstructors":true,
"methods":[{"name":"<init>","parameterTypes":[] }, {"name":"successfulExecutionDoesNotTerminateJVM","parameterTypes":[] }, {"name":"testCommandWithUnknownOption_37","parameterTypes":[] }, {"name":"testUnknownCommand_69","parameterTypes":[] }]
},
{
"name":"org.pgpainless.cli.TestUtils",
"allDeclaredClasses":true,
"queryAllDeclaredMethods":true,
"queryAllPublicMethods":true
},
{
"name":"org.pgpainless.cli.commands.ArmorCmdTest",
"allDeclaredFields":true,
"allDeclaredClasses":true,
"queryAllDeclaredMethods":true,
"queryAllPublicMethods":true,
"queryAllDeclaredConstructors":true,
"methods":[{"name":"<init>","parameterTypes":[] }, {"name":"armorAlreadyArmoredDataIsIdempotent","parameterTypes":[] }, {"name":"armorMessage","parameterTypes":[] }, {"name":"armorPublicKey","parameterTypes":[] }, {"name":"armorSecretKey","parameterTypes":[] }]
},
{
"name":"org.pgpainless.cli.commands.CLITest",
"allDeclaredFields":true,
"allDeclaredClasses":true,
"queryAllDeclaredMethods":true,
"queryAllPublicMethods":true,
"methods":[{"name":"cleanup","parameterTypes":[] }, {"name":"setup","parameterTypes":[] }]
},
{
"name":"org.pgpainless.cli.commands.DearmorCmdTest",
"allDeclaredFields":true,
"allDeclaredClasses":true,
"queryAllDeclaredMethods":true,
"queryAllPublicMethods":true,
"queryAllDeclaredConstructors":true,
"methods":[{"name":"<init>","parameterTypes":[] }, {"name":"dearmorBrokenArmoredKeyFails","parameterTypes":[] }, {"name":"dearmorCertificate","parameterTypes":[] }, {"name":"dearmorGarbageEmitsEmpty","parameterTypes":[] }, {"name":"dearmorMessage","parameterTypes":[] }, {"name":"dearmorSecretKey","parameterTypes":[] }]
},
{
"name":"org.pgpainless.cli.commands.ExtractCertCmdTest",
"allDeclaredFields":true,
"allDeclaredClasses":true,
"queryAllDeclaredMethods":true,
"queryAllPublicMethods":true,
"queryAllDeclaredConstructors":true,
"methods":[{"name":"<init>","parameterTypes":[] }, {"name":"extractCertFromGarbageFails","parameterTypes":[] }, {"name":"testExtractCert","parameterTypes":[] }, {"name":"testExtractCertFromCertFails","parameterTypes":[] }, {"name":"testExtractCertUnarmored","parameterTypes":[] }]
},
{
"name":"org.pgpainless.cli.commands.GenerateKeyCmdTest",
"allDeclaredFields":true,
"allDeclaredClasses":true,
"queryAllDeclaredMethods":true,
"queryAllPublicMethods":true,
"queryAllDeclaredConstructors":true,
"methods":[{"name":"<init>","parameterTypes":[] }, {"name":"testGenerateBinaryKey","parameterTypes":[] }, {"name":"testGenerateKey","parameterTypes":[] }, {"name":"testGenerateKeyWithMultipleUserIds","parameterTypes":[] }, {"name":"testGeneratePasswordProtectedKey_missingPasswordFile","parameterTypes":[] }, {"name":"testPasswordProtectedKey","parameterTypes":[] }]
},
{
"name":"org.pgpainless.cli.commands.InlineDetachCmdTest",
"allDeclaredFields":true,
"allDeclaredClasses":true,
"queryAllDeclaredMethods":true,
"queryAllPublicMethods":true,
"queryAllDeclaredConstructors":true,
"methods":[{"name":"<init>","parameterTypes":[] }, {"name":"detachInbandSignatureAndMessage","parameterTypes":[] }, {"name":"detachInbandSignatureAndMessageNoArmor","parameterTypes":[] }, {"name":"detachMissingSignaturesFromCleartextSignedMessageFails","parameterTypes":[] }, {"name":"detachNonOpenPgpDataFails","parameterTypes":[] }, {"name":"existingSignatureOutCausesException","parameterTypes":[] }]
},
{
"name":"org.pgpainless.cli.commands.ListProfilesCmdTest",
"allDeclaredFields":true,
"allDeclaredClasses":true,
"queryAllDeclaredMethods":true,
"queryAllPublicMethods":true,
"queryAllDeclaredConstructors":true,
"methods":[{"name":"<init>","parameterTypes":[] }, {"name":"listProfileOfGenerateKey","parameterTypes":[] }, {"name":"listProfilesOfEncrypt","parameterTypes":[] }, {"name":"listProfilesWithoutCommand","parameterTypes":[] }]
},
{
"name":"org.pgpainless.cli.commands.RoundTripEncryptDecryptCmdTest",
"allDeclaredFields":true,
"allDeclaredClasses":true,
"queryAllDeclaredMethods":true,
"queryAllPublicMethods":true,
"queryAllDeclaredConstructors":true,
"methods":[{"name":"<init>","parameterTypes":[] }, {"name":"decryptGarbageFails","parameterTypes":[] }, {"name":"decryptMalformedMessageYieldsBadData","parameterTypes":[] }, {"name":"decryptMessageWithSessionKey","parameterTypes":[] }, {"name":"decryptMessageWithWrongKeyFails","parameterTypes":[] }, {"name":"decryptWithPasswordWithPendingWhitespaceWorks","parameterTypes":[] }, {"name":"decryptWithWhitespacePasswordWorks","parameterTypes":[] }, {"name":"decrypt_verifyWithGarbageCertFails","parameterTypes":[] }, {"name":"decrypt_withGarbageKeyFails","parameterTypes":[] }, {"name":"encryptAndDecryptAMessage","parameterTypes":[] }, {"name":"encryptAndDecryptMessageWithPassphrase","parameterTypes":[] }, {"name":"encryptWithGarbageCertFails","parameterTypes":[] }, {"name":"encryptWithPasswordADecryptWithPasswordBFails","parameterTypes":[] }, {"name":"encryptWithProtectedKey_wrongPassphraseFails","parameterTypes":[] }, {"name":"encryptWithTrailingWhitespaceDecryptWithoutWorks","parameterTypes":[] }, {"name":"encrypt_signWithGarbageKeyFails","parameterTypes":[] }, {"name":"testDecryptVerifyOut_withoutVerifyWithFails","parameterTypes":[] }, {"name":"testDecryptWithSessionKeyVerifyWithYieldsExpectedVerifications","parameterTypes":[] }, {"name":"testDecryptWithoutDecryptionOptionFails","parameterTypes":[] }, {"name":"testEncryptDecryptRoundTripWithPasswordProtectedKey","parameterTypes":[] }, {"name":"testEncryptDecryptWithFreshRSAKey","parameterTypes":[] }, {"name":"testEncryptWithIncapableCert","parameterTypes":[] }, {"name":"testEncrypt_SignWithCertFails","parameterTypes":[] }, {"name":"testMissingArgumentsIfNoArgsSupplied","parameterTypes":[] }, {"name":"testSessionKeyOutWritesSessionKeyOut","parameterTypes":[] }, {"name":"testSignWithIncapableKey","parameterTypes":[] }, {"name":"testVerificationsOutAlreadyExistFails","parameterTypes":[] }]
},
{
"name":"org.pgpainless.cli.commands.RoundTripInlineSignInlineVerifyCmdTest",
"allDeclaredFields":true,
"allDeclaredClasses":true,
"queryAllDeclaredMethods":true,
"queryAllPublicMethods":true,
"queryAllDeclaredConstructors":true,
"methods":[{"name":"<init>","parameterTypes":[] }, {"name":"cannotVerifyEncryptedMessage","parameterTypes":[] }, {"name":"cannotVerifyMalformedMessage","parameterTypes":[] }, {"name":"createAndVerifyCleartextSignedMessage","parameterTypes":[] }, {"name":"createAndVerifyMultiKeyBinarySignedMessage","parameterTypes":[] }, {"name":"createAndVerifyTextSignedMessage","parameterTypes":[] }, {"name":"createCleartextSignedMessage","parameterTypes":[] }, {"name":"createMalformedMessage","parameterTypes":[] }, {"name":"createSignedMessageWithKeyAAndVerifyWithKeyBFails","parameterTypes":[] }, {"name":"createTextSignedMessageInlineDetachAndDetachedVerify","parameterTypes":[] }, {"name":"signWithProtectedKeyWithWrongPassphraseFails","parameterTypes":[] }, {"name":"testInlineSignWithMissingSecretKeysFails","parameterTypes":[] }, {"name":"testUnlockKeyWithOneOfMultiplePasswords","parameterTypes":[] }, {"name":"verifyPrependedSignedMessage","parameterTypes":[] }]
},
{
"name":"org.pgpainless.cli.commands.RoundTripInlineSignVerifyCmdTest",
"allDeclaredFields":true,
"allDeclaredClasses":true,
"queryAllDeclaredMethods":true,
"queryAllPublicMethods":true,
"queryAllDeclaredConstructors":true,
"methods":[{"name":"<init>","parameterTypes":[] }, {"name":"encryptAndDecryptAMessage","parameterTypes":[] }]
},
{
"name":"org.pgpainless.cli.commands.RoundTripSignVerifyCmdTest",
"allDeclaredFields":true,
"allDeclaredClasses":true,
"queryAllDeclaredMethods":true,
"queryAllPublicMethods":true,
"queryAllDeclaredConstructors":true,
"methods":[{"name":"<init>","parameterTypes":[] }, {"name":"createArmoredSignature","parameterTypes":[] }, {"name":"createUnarmoredSignature","parameterTypes":[] }, {"name":"signWithProtectedKey","parameterTypes":[] }, {"name":"signWithProtectedKey_missingPassphraseFails","parameterTypes":[] }, {"name":"signWithProtectedKey_wrongPassphraseFails","parameterTypes":[] }, {"name":"testNotAfter","parameterTypes":[] }, {"name":"testNotBefore","parameterTypes":[] }, {"name":"testSignWithIncapableKey","parameterTypes":[] }, {"name":"testSignatureCreationAndVerification","parameterTypes":[] }, {"name":"unarmorArmoredSigAndVerify","parameterTypes":[] }]
},
{
"name":"org.pgpainless.cli.commands.VersionCmdTest",
"allDeclaredFields":true,
"allDeclaredClasses":true,
"queryAllDeclaredMethods":true,
"queryAllPublicMethods":true,
"queryAllDeclaredConstructors":true,
"methods":[{"name":"<init>","parameterTypes":[] }, {"name":"testExtendedVersion","parameterTypes":[] }, {"name":"testGetBackendVersion","parameterTypes":[] }, {"name":"testSopSpecVersion","parameterTypes":[] }, {"name":"testVersion","parameterTypes":[] }]
},
{
"name":"org.pgpainless.cli.misc.SignUsingPublicKeyBehaviorTest",
"allDeclaredFields":true,
"allDeclaredClasses":true,
"queryAllDeclaredMethods":true,
"queryAllPublicMethods":true,
"queryAllDeclaredConstructors":true,
"methods":[{"name":"<init>","parameterTypes":[] }, {"name":"testSignatureCreationAndVerification","parameterTypes":[] }]
},
{
"name":"picocli.AutoComplete$GenerateCompletion",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"picocli.CommandLine$AutoHelpMixin",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true
},
{
"name":"picocli.CommandLine$HelpCommand",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true
},
{
"name":"sop.cli.picocli.SopCLI",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"sop.cli.picocli.SopCLI$InitLocale",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true
},
{
"name":"sop.cli.picocli.commands.AbstractSopCmd",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true
},
{
"name":"sop.cli.picocli.commands.ArmorCmd",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"sop.cli.picocli.commands.ChangeKeyPasswordCmd",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true
},
{
"name":"sop.cli.picocli.commands.DearmorCmd",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"sop.cli.picocli.commands.DecryptCmd",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"sop.cli.picocli.commands.EncryptCmd",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"sop.cli.picocli.commands.ExtractCertCmd",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"sop.cli.picocli.commands.GenerateKeyCmd",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"sop.cli.picocli.commands.InlineDetachCmd",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"sop.cli.picocli.commands.InlineSignCmd",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"sop.cli.picocli.commands.InlineVerifyCmd",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"sop.cli.picocli.commands.ListProfilesCmd",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"sop.cli.picocli.commands.RevokeKeyCmd",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true
},
{
"name":"sop.cli.picocli.commands.SignCmd",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"sop.cli.picocli.commands.VerifyCmd",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"sop.cli.picocli.commands.VersionCmd",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"sop.cli.picocli.commands.VersionCmd$Exclusive",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"sun.security.provider.NativePRNG",
"methods":[{"name":"<init>","parameterTypes":[] }, {"name":"<init>","parameterTypes":["java.security.SecureRandomParameters"] }]
},
{
"name":"sun.security.provider.SHA",
"methods":[{"name":"<init>","parameterTypes":[] }]
}
]

View file

@ -1,93 +0,0 @@
{
"resources":{
"includes":[{
"pattern":"\\QMETA-INF/services/ch.qos.logback.classic.spi.Configurator\\E"
}, {
"pattern":"\\QMETA-INF/services/java.lang.System$LoggerFinder\\E"
}, {
"pattern":"\\QMETA-INF/services/java.nio.channels.spi.SelectorProvider\\E"
}, {
"pattern":"\\QMETA-INF/services/java.time.zone.ZoneRulesProvider\\E"
}, {
"pattern":"\\QMETA-INF/services/java.util.spi.ResourceBundleControlProvider\\E"
}, {
"pattern":"\\QMETA-INF/services/javax.xml.parsers.SAXParserFactory\\E"
}, {
"pattern":"\\QMETA-INF/services/org.junit.platform.engine.TestEngine\\E"
}, {
"pattern":"\\QMETA-INF/services/org.junit.platform.launcher.LauncherDiscoveryListener\\E"
}, {
"pattern":"\\QMETA-INF/services/org.junit.platform.launcher.LauncherSessionListener\\E"
}, {
"pattern":"\\QMETA-INF/services/org.junit.platform.launcher.PostDiscoveryFilter\\E"
}, {
"pattern":"\\QMETA-INF/services/org.junit.platform.launcher.TestExecutionListener\\E"
}, {
"pattern":"\\QMETA-INF/services/org.slf4j.spi.SLF4JServiceProvider\\E"
}, {
"pattern":"\\Qjunit-platform.properties\\E"
}, {
"pattern":"\\Qlogback-test.scmo\\E"
}, {
"pattern":"\\Qlogback-test.xml\\E"
}, {
"pattern":"\\Qlogback.scmo\\E"
}, {
"pattern":"\\Qorg/slf4j/impl/StaticLoggerBinder.class\\E"
}, {
"pattern":"\\Qpgpainless-sop.properties\\E"
}, {
"pattern":"\\Qsop-java-version.properties\\E"
}, {
"pattern":"java.base:\\Qsun/text/resources/LineBreakIteratorData\\E"
}]},
"bundles":[{
"name":"msg_armor",
"locales":["de", "und"]
}, {
"name":"msg_change-key-password",
"locales":["de", "und"]
}, {
"name":"msg_dearmor",
"locales":["de", "und"]
}, {
"name":"msg_decrypt",
"locales":["de", "und"]
}, {
"name":"msg_detached-sign",
"locales":["de", "und"]
}, {
"name":"msg_detached-verify",
"locales":["de", "und"]
}, {
"name":"msg_encrypt",
"locales":["de", "und"]
}, {
"name":"msg_extract-cert",
"locales":["de", "und"]
}, {
"name":"msg_generate-key",
"locales":["de", "und"]
}, {
"name":"msg_inline-detach",
"locales":["de", "und"]
}, {
"name":"msg_inline-sign",
"locales":["de", "und"]
}, {
"name":"msg_inline-verify",
"locales":["de", "und"]
}, {
"name":"msg_list-profiles",
"locales":["de", "und"]
}, {
"name":"msg_revoke-key",
"locales":["de", "und"]
}, {
"name":"msg_sop",
"locales":["de", "und"]
}, {
"name":"msg_version",
"locales":["de", "und"]
}]
}

View file

@ -1,41 +0,0 @@
{
"types":[
{
"name":"java.lang.Enum"
},
{
"name":"java.lang.Object[]"
},
{
"name":"java.util.HashSet"
},
{
"name":"java.util.LinkedHashSet"
},
{
"name":"java.util.concurrent.ArrayBlockingQueue"
},
{
"name":"java.util.concurrent.locks.AbstractOwnableSynchronizer"
},
{
"name":"java.util.concurrent.locks.AbstractQueuedSynchronizer"
},
{
"name":"java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject"
},
{
"name":"java.util.concurrent.locks.ReentrantLock"
},
{
"name":"java.util.concurrent.locks.ReentrantLock$NonfairSync"
},
{
"name":"java.util.concurrent.locks.ReentrantLock$Sync"
}
],
"lambdaCapturingTypes":[
],
"proxies":[
]
}

View file

@ -5,5 +5,22 @@ SPDX-License-Identifier: Apache-2.0
-->
<configuration debug="false">
<statusListener class="ch.qos.logback.core.status.NopStatusListener" />
<appender name="STDERR" class="ch.qos.logback.core.ConsoleAppender">
<target>System.err</target>
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<target>System.out</target>
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="error">
<appender-ref ref="STDERR" />
</root>
</configuration>

View file

@ -4,35 +4,28 @@
package org.pgpainless.cli;
import com.ginsberg.junit.exit.ExpectSystemExitWithStatus;
import com.ginsberg.junit.exit.FailOnSystemExit;
import org.junit.jupiter.api.Test;
import org.pgpainless.cli.commands.CLITest;
import org.slf4j.LoggerFactory;
import sop.exception.SOPGPException;
import java.io.IOException;
public class ExitCodeTest {
import static org.junit.jupiter.api.Assertions.assertEquals;
public class ExitCodeTest extends CLITest {
public ExitCodeTest() {
super(LoggerFactory.getLogger(ExitCodeTest.class));
@Test
@ExpectSystemExitWithStatus(SOPGPException.UnsupportedSubcommand.EXIT_CODE)
public void testUnknownCommand_69() {
PGPainlessCLI.main(new String[] {"generate-kex"});
}
@Test
public void testUnknownCommand_69() throws IOException {
assertEquals(SOPGPException.UnsupportedSubcommand.EXIT_CODE,
executeCommand("unsupported-subcommand"));
@ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE)
public void testCommandWithUnknownOption_37() {
PGPainlessCLI.main(new String[] {"generate-key", "-k", "\"k is unknown\""});
}
@Test
public void testCommandWithUnknownOption_37() throws IOException {
assertEquals(SOPGPException.UnsupportedOption.EXIT_CODE,
executeCommand("generate-key", "-k", "\"k is unknown\""));
}
@Test
public void successfulExecutionDoesNotTerminateJVM() throws IOException {
assertSuccess(executeCommand("version"));
@FailOnSystemExit
public void successfulExecutionDoesNotTerminateJVM() {
PGPainlessCLI.main(new String[] {"version"});
}
}

View file

@ -16,6 +16,7 @@ import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.junit.jupiter.api.Test;
import org.pgpainless.PGPainless;
import org.slf4j.LoggerFactory;
import sop.exception.SOPGPException;
public class ArmorCmdTest extends CLITest {
@ -89,12 +90,11 @@ public class ArmorCmdTest extends CLITest {
}
@Test
public void armorAlreadyArmoredDataIsIdempotent() throws IOException {
pipeStringToStdin(key);
ByteArrayOutputStream armorOut = pipeStdoutToStream();
assertSuccess(executeCommand("armor"));
String armored = armorOut.toString();
assertEquals(key, armored);
public void labelNotYetSupported() throws IOException {
pipeStringToStdin("Hello, World!\n");
ByteArrayOutputStream out = pipeStdoutToStream();
int exitCode = executeCommand("armor", "--label", "Message");
assertEquals(SOPGPException.UnsupportedOption.EXIT_CODE, exitCode);
assertEquals(0, out.size());
}
}

View file

@ -161,7 +161,6 @@ public abstract class CLITest {
}
public void assertSuccess(int exitCode) {
assertEquals(0, exitCode,
"Expected successful program execution");
assertEquals(0, exitCode, "Expected successful program execution");
}
}

View file

@ -6,7 +6,6 @@ package org.pgpainless.cli.commands;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
@ -16,7 +15,6 @@ import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.junit.jupiter.api.Test;
import org.pgpainless.PGPainless;
import org.slf4j.LoggerFactory;
import sop.exception.SOPGPException;
public class DearmorCmdTest extends CLITest {
@ -60,17 +58,6 @@ public class DearmorCmdTest extends CLITest {
assertArrayEquals(secretKey.getEncoded(), dearmored.toByteArray());
}
@Test
public void dearmorBrokenArmoredKeyFails() throws IOException {
// contains a "-"
String invalidBase64 = "lFgEY2vOkhYJKwYBBAHaRw8BAQdAqGOtLd1tKnuwaYYcdr2/7C0cPiCCggRMKG+Wt32QQdEAAP9VaBzjk/AaAqyykZnQHmS1HByEvRLv5/4yJMSr22451BFjtBRhbGljZUBwZ3BhaW5sZXNzLm9yZ4iOBBMWCgBBBQJja86SCRCLB1F3AflTTBYhBGLp3aTyD4NB0rxLT-IsHUXcB+VNMAp4BApsBBRYCAwEABAsJCAcFFQoJCAsCmQEAACZhAP4s8hn/RBDvyLvGROOd15EYATnWlgyi+b5WXP6cELalJwD1FZy3RROhfNtZWcJPS43fG03pYNyb0NXoitIMAaXEB5xdBGNrzpISCisGAQQBl1UBBQEBB0CqCcYethOynfni8uRO+r/cZWp9hCLy8pRIExKqzcyEFAMBCAcAAP9sRRLoZkLpDaTNNrtIBovXu2ANhL8keUMWtVcuEHnkQA6iiHUEGBYKAB0FAmNrzpICngECmwwFFgIDAQAECwkIBwUVCgkICwAKCRCLB1F3AflTTBVpAP491etrjqCMWx2bBaw3K1vP0Mix6U0vF3J4kP9UeZm6owEA4kX9VAGESvLgIc7CEiswmxdWjxnLQyCRtWXfjgFmYQucWARja86SFgkrBgEEAdpHDwEBB0DBslhDpWC6CV3xJUSo071NSO5Cf4fgOwOj+QHs8mpFbwABAPkQioSydYiMi04LyfPohyrhhcdJDHallQg+jYHHUb2pEJCI1QQYFgoAfQUCY2vOkgKeAQKbAgUWAgMBAAQLCQgHBRUKCQgLXyAEGRYKAAYFAmNrzpIACgkQiHlkvEXh+f1eywEA9A2GLU9LxCJxZf2X4qcZY//YJDChIZHPnY0Vaek1DsMBAN1YILrH2rxQeCXjm4bUKfJIRrGt6ZJscwORgNI1dFQFAAoJEIsHUXcB+VNMK3gA/3vvPm57JsHA860wlB4D1II71oFNL8TFnJqTAvpSKe1AAP49S4mKB4PE0ElcDo7n+nEYt6ba8IMRDlMorsH85mUgCw==";
pipeStringToStdin(invalidBase64);
ByteArrayOutputStream out = pipeStdoutToStream();
int exitCode = executeCommand("dearmor");
assertEquals(SOPGPException.BadData.EXIT_CODE, exitCode);
assertEquals(0, out.size());
}
@Test
public void dearmorCertificate() throws IOException {
@ -100,14 +87,4 @@ public class DearmorCmdTest extends CLITest {
assertEquals("Hello, World\n", out.toString());
}
@Test
public void dearmorGarbageEmitsEmpty() throws IOException {
String noArmoredData = "This is not armored.";
pipeStringToStdin(noArmoredData);
ByteArrayOutputStream out = pipeStdoutToStream();
assertSuccess(executeCommand("dearmor"));
assertTrue(out.toString().isEmpty());
}
}

View file

@ -30,8 +30,7 @@ public class ExtractCertCmdTest extends CLITest {
}
@Test
public void testExtractCert()
throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException, IOException {
public void testExtractCert() throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException, IOException {
PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing()
.simpleEcKeyRing("Juliet Capulet <juliet@capulet.lit>");

View file

@ -83,8 +83,7 @@ public class GenerateKeyCmdTest extends CLITest {
KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys);
assertTrue(info.isFullyEncrypted());
assertNotNull(UnlockSecretKey
.unlockSecretKey(secretKeys.getSecretKey(), Passphrase.fromPassword("sw0rdf1sh")));
assertNotNull(UnlockSecretKey.unlockSecretKey(secretKeys.getSecretKey(), Passphrase.fromPassword("sw0rdf1sh")));
}
@Test
@ -92,7 +91,6 @@ public class GenerateKeyCmdTest extends CLITest {
int exit = executeCommand("generate-key",
"--with-key-password", "nonexistent", "Alice <alice@pgpainless.org>");
assertEquals(SOPGPException.MissingInput.EXIT_CODE, exit,
"Expected MISSING_INPUT (" + SOPGPException.MissingInput.EXIT_CODE + ")");
assertEquals(SOPGPException.MissingInput.EXIT_CODE, exit, "Expected MISSING_INPUT (" + SOPGPException.MissingInput.EXIT_CODE + ")");
}
}

View file

@ -90,7 +90,7 @@ public class InlineDetachCmdTest extends CLITest {
pipeStringToStdin(msgOut.toString());
ByteArrayOutputStream verifyOut = pipeStdoutToStream();
assertSuccess(executeCommand("verify", sigFile.getAbsolutePath(), certFile.getAbsolutePath()));
assertEquals("2021-05-15T16:08:06Z 4F665C4DC2C4660BC6425E415736E6931ACF370C 4F665C4DC2C4660BC6425E415736E6931ACF370C mode:text\n",
assertEquals("2021-05-15T16:08:06Z 4F665C4DC2C4660BC6425E415736E6931ACF370C 4F665C4DC2C4660BC6425E415736E6931ACF370C\n",
verifyOut.toString());
}
@ -115,7 +115,7 @@ public class InlineDetachCmdTest extends CLITest {
ByteArrayOutputStream verifyOut = pipeStdoutToStream();
File certFile = writeFile("cert.asc", CERT);
assertSuccess(executeCommand("verify", sigFile.getAbsolutePath(), certFile.getAbsolutePath()));
assertEquals("2021-05-15T16:08:06Z 4F665C4DC2C4660BC6425E415736E6931ACF370C 4F665C4DC2C4660BC6425E415736E6931ACF370C mode:text\n",
assertEquals("2021-05-15T16:08:06Z 4F665C4DC2C4660BC6425E415736E6931ACF370C 4F665C4DC2C4660BC6425E415736E6931ACF370C\n",
verifyOut.toString());
}
@ -129,27 +129,4 @@ public class InlineDetachCmdTest extends CLITest {
assertEquals(0, msgOut.size());
}
@Test
public void detachNonOpenPgpDataFails() throws IOException {
File sig = nonExistentFile("sig.asc");
pipeStringToStdin("This is non-OpenPGP data and therefore we cannot detach any signatures from it.");
int exitCode = executeCommand("inline-detach", "--signatures-out", sig.getAbsolutePath());
assertEquals(SOPGPException.BadData.EXIT_CODE, exitCode);
}
@Test
public void detachMissingSignaturesFromCleartextSignedMessageFails() throws IOException {
String cleartextSignedNoSigs = "-----BEGIN PGP SIGNED MESSAGE-----\n" +
"\n" +
"Hello, World!\n" +
"What's Up!??\n" +
"\n" +
"\n";
pipeStringToStdin(cleartextSignedNoSigs);
File sig = nonExistentFile("sig.asc");
int exitCode = executeCommand("inline-detach", "--signatures-out", sig.getAbsolutePath());
assertEquals(SOPGPException.BadData.EXIT_CODE, exitCode);
}
}

View file

@ -1,42 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.cli.commands;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import org.junit.jupiter.api.Test;
import org.slf4j.LoggerFactory;
public class ListProfilesCmdTest extends CLITest {
public ListProfilesCmdTest() {
super(LoggerFactory.getLogger(ListProfilesCmdTest.class));
}
@Test
public void listProfilesWithoutCommand() throws IOException {
assertNotEquals(0, executeCommand("list-profiles"));
}
@Test
public void listProfileOfGenerateKey() throws IOException {
ByteArrayOutputStream output = pipeStdoutToStream();
assertSuccess(executeCommand("list-profiles", "generate-key"));
assertTrue(output.toString().contains("rfc4880"));
}
@Test
public void listProfilesOfEncrypt() throws IOException {
ByteArrayOutputStream output = pipeStdoutToStream();
assertSuccess(executeCommand("list-profiles", "encrypt"));
assertTrue(output.toString().contains("rfc4880"));
}
}

View file

@ -14,17 +14,17 @@ import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException;
import com.ginsberg.junit.exit.FailOnSystemExit;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.pgpainless.PGPainless;
import org.pgpainless.algorithm.KeyFlag;
import org.pgpainless.key.generation.KeySpec;
import org.pgpainless.key.generation.type.KeyType;
import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve;
import org.pgpainless.key.generation.type.xdh_legacy.XDHLegacySpec;
import org.pgpainless.key.generation.type.eddsa.EdDSACurve;
import org.pgpainless.key.generation.type.xdh.XDHSpec;
import org.slf4j.LoggerFactory;
import sop.exception.SOPGPException;
@ -82,6 +82,7 @@ public class RoundTripEncryptDecryptCmdTest extends CLITest {
"-----END PGP PUBLIC KEY BLOCK-----";
@Test
@FailOnSystemExit
public void encryptAndDecryptAMessage() throws IOException {
// Juliets key and cert
File julietKeyFile = pipeStdoutToFile("juliet.key");
@ -128,7 +129,7 @@ public class RoundTripEncryptDecryptCmdTest extends CLITest {
String romeosVerif = readStringFromFile(anotherVerificationsFile);
assertEquals(julietsVerif, romeosVerif);
assertFalse(julietsVerif.isEmpty());
assertEquals(115, julietsVerif.length()); // 115 is number of symbols in [DATE, FINGER, FINGER, MODE] for V4
assertEquals(103, julietsVerif.length()); // 103 is number of symbols in [DATE, FINGER, FINGER] for V4
}
@Test
@ -138,7 +139,6 @@ public class RoundTripEncryptDecryptCmdTest extends CLITest {
}
@Test
@Disabled("Disabled, since we now read certificates from secret keys")
public void testEncryptingForKeyFails() throws IOException {
File notACert = writeFile("key.asc", KEY);
@ -168,8 +168,7 @@ public class RoundTripEncryptDecryptCmdTest extends CLITest {
File verifications = nonExistentFile("verifications");
ByteArrayOutputStream out = pipeStdoutToStream();
int exitCode = executeCommand("decrypt", "--verifications-out",
verifications.getAbsolutePath(), key.getAbsolutePath());
int exitCode = executeCommand("decrypt", "--verifications-out", verifications.getAbsolutePath(), key.getAbsolutePath());
assertEquals(SOPGPException.IncompleteVerification.EXIT_CODE, exitCode);
assertEquals(0, out.size());
@ -212,8 +211,7 @@ public class RoundTripEncryptDecryptCmdTest extends CLITest {
pipeStringToStdin(ciphertext);
ByteArrayOutputStream plaintextOut = pipeStdoutToStream();
assertSuccess(executeCommand("decrypt", "--session-key-out",
sessionKeyFile.getAbsolutePath(), key.getAbsolutePath()));
assertSuccess(executeCommand("decrypt", "--session-key-out", sessionKeyFile.getAbsolutePath(), key.getAbsolutePath()));
assertEquals(plaintext, plaintextOut.toString());
String resultSessionKey = readStringFromFile(sessionKeyFile);
@ -274,7 +272,7 @@ public class RoundTripEncryptDecryptCmdTest extends CLITest {
assertEquals(plaintext, out.toString());
String verificationString = readStringFromFile(verifications);
assertEquals("2022-11-09T17:22:48Z C0DCEC44B1A173664B05DABCECD0BF863F65C9A5 A2EC077FC977E15DD799EFF92C0D3C123CF51C08 mode:binary\n",
assertEquals("2022-11-09T17:22:48Z C0DCEC44B1A173664B05DABCECD0BF863F65C9A5 A2EC077FC977E15DD799EFF92C0D3C123CF51C08\n",
verificationString);
}
@ -302,8 +300,7 @@ public class RoundTripEncryptDecryptCmdTest extends CLITest {
InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException {
PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing()
.addUserId("No Crypt <no@crypt.key>")
.setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519),
KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA))
.setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA))
.build();
PGPPublicKeyRing cert = PGPainless.extractCertificate(secretKeys);
File certFile = writeFile("cert.pgp", cert.getEncoded());
@ -317,13 +314,11 @@ public class RoundTripEncryptDecryptCmdTest extends CLITest {
}
@Test
public void testSignWithIncapableKey()
throws IOException, PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException {
public void testSignWithIncapableKey() throws IOException, PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException {
PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing()
.addUserId("Cannot Sign <cannot@sign.key>")
.setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER))
.addSubkey(KeySpec.getBuilder(
KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE))
.setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER))
.addSubkey(KeySpec.getBuilder(KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE))
.build();
File keyFile = writeFile("key.pgp", secretKeys.getEncoded());
File certFile = writeFile("cert.pgp", PGPainless.extractCertificate(secretKeys).getEncoded());
@ -563,111 +558,4 @@ public class RoundTripEncryptDecryptCmdTest extends CLITest {
keyFile.getAbsolutePath()));
assertEquals(msg, out.toString());
}
@Test
public void decryptMalformedMessageYieldsBadData() throws IOException {
// Message contains encrypted data packet which contains the plaintext directly - no literal data packet.
// It is therefore malformed.
String malformed = "-----BEGIN PGP MESSAGE-----\n" +
"Version: BCPG v1.72b04\n" +
"\n" +
"hF4D831k4umlLu4SAQdApKA6VDKSLQvwS2kbWqlhcXD8XHdFkSccqv5tBptZnBgw\n" +
"nZNXVhwUpap0ymb4jPTD+EVPKOfPyy04ouIGZAJKkfYDeSL/8sKcbnPPuQJYYjGQ\n" +
"ySDNmidrtTonwcSuwAfRyn74BBqOVhrr8GXkVIfevIlZFQ==\n" +
"=wIgl\n" +
"-----END PGP MESSAGE-----";
File key = writeFile("key.asc", KEY);
pipeStringToStdin(malformed);
int exitCode = executeCommand("decrypt", key.getAbsolutePath());
assertEquals(SOPGPException.BadData.EXIT_CODE, exitCode);
}
@Test
public void decryptWithPasswordWithPendingWhitespaceWorks() throws IOException {
assertEncryptWithPasswordADecryptWithPasswordBWorks("sw0rdf1sh", "sw0rdf1sh \n");
}
@Test
public void encryptWithTrailingWhitespaceDecryptWithoutWorks() throws IOException {
assertEncryptWithPasswordADecryptWithPasswordBWorks("sw0rdf1sh \n", "sw0rdf1sh");
}
@Test
public void decryptWithWhitespacePasswordWorks() throws IOException {
// is encrypted for "sw0rdf1sh \n"
String encryptedToPasswordWithTrailingWhitespace = "-----BEGIN PGP MESSAGE-----\n" +
"\n" +
"jA0ECQMC32tEJug0BCpg0kABfT3dKgA4K8XGpk2ul67BXLZD//fCCSmIQIWnNhE1\n" +
"6q97xFQ628K8f/58+XoBzLqLDT+LEz9Bz+Yg9QfzkEFy\n" +
"=2Y+K\n" +
"-----END PGP MESSAGE-----";
pipeStringToStdin(encryptedToPasswordWithTrailingWhitespace);
File password = writeFile("password", "sw0rdf1sh \n");
ByteArrayOutputStream plaintext = pipeStdoutToStream();
assertSuccess(executeCommand("decrypt", "--with-password", password.getAbsolutePath()));
assertEquals("Hello, World!\n", plaintext.toString());
}
private void assertEncryptWithPasswordADecryptWithPasswordBWorks(String passwordA, String passwordB)
throws IOException {
File passwordAFile = writeFile("password", passwordA);
File passwordBFile = writeFile("passwordWithWS", passwordB);
String msg = "Hello, World!\n";
pipeStringToStdin(msg);
ByteArrayOutputStream ciphertext = pipeStdoutToStream();
assertSuccess(executeCommand("encrypt", "--with-password", passwordAFile.getAbsolutePath()));
pipeStringToStdin(ciphertext.toString());
ByteArrayOutputStream plaintext = pipeStdoutToStream();
assertSuccess(executeCommand("decrypt", "--with-password", passwordBFile.getAbsolutePath()));
assertEquals(msg, plaintext.toString());
}
@Test
public void testDecryptWithoutDecryptionOptionFails() throws IOException {
String ciphertext = "-----BEGIN PGP MESSAGE-----\n" +
"Version: PGPainless\n" +
"\n" +
"hF4D831k4umlLu4SAQdAYisjZTDRm217LHQbqjB766tm62CKTkRj3Gd0wYxVRCgw\n" +
"48SnOJINCJoPgDsxk2NiJmLCImoiET7IElqxN9htdDXQJwcRK+71r/ZyO4YJpWuX\n" +
"0sAAAcEFc3nT+un31sOi8KoBJlc5n+MemntQvcWDs8B87BEW/Ncjrs0s4pJpZKBQ\n" +
"/AWc4wLCI3ylfMQJB2pICqaOO3KP3WepgTIw5fuZm6YfriKQi7uZvVx1N+uaCIoa\n" +
"K2IVVf/7O9KZJ9GbsGYdpBj9IdaIZiVS3Xi8rwgQl3haI/EeHC3nnCsWyj23Fjt3\n" +
"LjbMqpHbSnp8U1cQ8rXavrREaKv69PFeJio6/hRg32TzJqn05dPALRxHMEkxxa4h\n" +
"FpVU\n" +
"=edS5\n" +
"-----END PGP MESSAGE-----";
pipeStringToStdin(ciphertext);
int exitCode = executeCommand("decrypt");
assertEquals(SOPGPException.MissingArg.EXIT_CODE, exitCode);
}
@Test
public void testEncryptDecryptWithFreshRSAKey() throws IOException {
// Generate key
File passwordFile = writeFile("password", "sw0rdf1sh");
File keyFile = pipeStdoutToFile("key.asc");
assertSuccess(executeCommand("generate-key", "--profile=rfc4880", "--with-key-password", passwordFile.getAbsolutePath(), "Alice <alice@example.org>"));
File certFile = pipeStdoutToFile("cert.asc");
pipeFileToStdin(keyFile);
assertSuccess(executeCommand("extract-cert"));
// Write plaintext
File plaintextFile = writeFile("msg.txt", "Hello, World!\n");
// Encrypt
File ciphertextFile = pipeStdoutToFile("msg.asc");
pipeFileToStdin(plaintextFile);
assertSuccess(executeCommand("encrypt", "--profile=rfc4880", certFile.getAbsolutePath()));
ByteArrayOutputStream decrypted = pipeStdoutToStream();
pipeFileToStdin(ciphertextFile);
assertSuccess(executeCommand("decrypt", "--with-key-password", passwordFile.getAbsolutePath(), keyFile.getAbsolutePath()));
assertEquals("Hello, World!\n", decrypted.toString());
}
}

View file

@ -4,28 +4,16 @@
package org.pgpainless.cli.commands;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import org.bouncycastle.bcpg.ArmoredOutputStream;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.openpgp.PGPSignature;
import org.junit.jupiter.api.Test;
import org.pgpainless.PGPainless;
import org.pgpainless.algorithm.CompressionAlgorithm;
import org.pgpainless.encryption_signing.EncryptionStream;
import org.pgpainless.encryption_signing.ProducerOptions;
import org.pgpainless.encryption_signing.SigningOptions;
import org.pgpainless.key.protection.SecretKeyRingProtector;
import org.slf4j.LoggerFactory;
import sop.exception.SOPGPException;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class RoundTripInlineSignInlineVerifyCmdTest extends CLITest {
@ -138,10 +126,6 @@ public class RoundTripInlineSignInlineVerifyCmdTest extends CLITest {
"\n" +
"There is only one Lord of the Keys, only one who can bend them to his will. And he does not share power.";
private static final String MESSAGE_CRLF = "One does not simply use OpenPGP!\r\n" +
"\r\n" +
"There is only one Lord of the Keys, only one who can bend them to his will. And he does not share power.";
@Test
public void createCleartextSignedMessage() throws IOException {
File key = writeFile("key.asc", KEY_1);
@ -150,14 +134,14 @@ public class RoundTripInlineSignInlineVerifyCmdTest extends CLITest {
pipeStringToStdin(MESSAGE);
ByteArrayOutputStream ciphertextOut = pipeStdoutToStream();
assertSuccess(executeCommand("inline-sign",
"--as", "clearsigned",
"--as", "cleartextsigned",
key.getAbsolutePath(),
"--with-key-password", password.getAbsolutePath()));
String cleartextSigned = ciphertextOut.toString();
assertTrue(cleartextSigned.startsWith("-----BEGIN PGP SIGNED MESSAGE-----\n" +
"Hash: "));
assertTrue(cleartextSigned.contains(MESSAGE_CRLF));
assertTrue(cleartextSigned.contains(MESSAGE));
assertTrue(cleartextSigned.contains("\n-----BEGIN PGP SIGNATURE-----\n"));
assertTrue(cleartextSigned.endsWith("-----END PGP SIGNATURE-----\n"));
}
@ -170,7 +154,7 @@ public class RoundTripInlineSignInlineVerifyCmdTest extends CLITest {
pipeStringToStdin(MESSAGE);
ByteArrayOutputStream ciphertextOut = pipeStdoutToStream();
assertSuccess(executeCommand("inline-sign",
"--as", "clearsigned",
"--as", "cleartextsigned",
key.getAbsolutePath(),
"--with-key-password", password.getAbsolutePath()));
@ -187,55 +171,6 @@ public class RoundTripInlineSignInlineVerifyCmdTest extends CLITest {
assertTrue(verificationString.contains(CERT_1_SIGNING_KEY));
}
@Test
public void createAndVerifyTextSignedMessage() throws IOException {
File key = writeFile("key.asc", KEY_1);
File password = writeFile("password", KEY_1_PASSWORD);
pipeStringToStdin(MESSAGE);
ByteArrayOutputStream ciphertextOut = pipeStdoutToStream();
assertSuccess(executeCommand("inline-sign",
"--as", "text",
key.getAbsolutePath(),
"--with-key-password", password.getAbsolutePath()));
File cert = writeFile("cert.asc", CERT_1);
File verifications = nonExistentFile("verifications");
pipeStringToStdin(ciphertextOut.toString());
ByteArrayOutputStream plaintextOut = pipeStdoutToStream();
assertSuccess(executeCommand("inline-verify",
"--verifications-out", verifications.getAbsolutePath(),
cert.getAbsolutePath()));
assertEquals(MESSAGE_CRLF, plaintextOut.toString());
String verificationString = readStringFromFile(verifications);
assertTrue(verificationString.contains(CERT_1_SIGNING_KEY));
}
@Test
public void createSignedMessageWithKeyAAndVerifyWithKeyBFails() throws IOException {
File key = writeFile("key.asc", KEY_1);
File password = writeFile("password", KEY_1_PASSWORD);
File cert = writeFile("cert.asc", CERT_2); // mismatch
pipeStringToStdin(MESSAGE);
ByteArrayOutputStream ciphertextOut = pipeStdoutToStream();
assertSuccess(executeCommand("inline-sign",
key.getAbsolutePath(),
"--with-key-password", password.getAbsolutePath()));
File verifications = nonExistentFile("verifications");
pipeStringToStdin(ciphertextOut.toString());
ByteArrayOutputStream plaintextOut = pipeStdoutToStream();
int exitCode = executeCommand("inline-verify",
"--verifications-out", verifications.getAbsolutePath(),
cert.getAbsolutePath());
assertEquals(SOPGPException.NoSignature.EXIT_CODE, exitCode);
assertEquals(MESSAGE, plaintextOut.toString()); // message is emitted nonetheless
assertFalse(verifications.exists(), "Verifications file MUST NOT be written.");
}
@Test
public void createAndVerifyMultiKeyBinarySignedMessage() throws IOException {
File key1Pass = writeFile("password", KEY_1_PASSWORD);
@ -277,7 +212,7 @@ public class RoundTripInlineSignInlineVerifyCmdTest extends CLITest {
pipeStringToStdin(MESSAGE);
ByteArrayOutputStream ciphertextOut = pipeStdoutToStream();
assertSuccess(executeCommand("inline-sign",
"--as", "clearsigned",
"--as", "cleartextsigned",
key.getAbsolutePath(),
"--with-key-password", password.getAbsolutePath()));
@ -291,176 +226,11 @@ public class RoundTripInlineSignInlineVerifyCmdTest extends CLITest {
File cert = writeFile("cert.asc", CERT_1);
pipeStringToStdin(msgOut.toString());
ByteArrayOutputStream verificationsOut = pipeStdoutToStream();
assertSuccess(executeCommand("verify", "--stacktrace",
assertSuccess(executeCommand("verify",
sigFile.getAbsolutePath(),
cert.getAbsolutePath()));
String verificationString = verificationsOut.toString();
assertTrue(verificationString.contains(CERT_1_SIGNING_KEY));
}
@Test
public void testUnlockKeyWithOneOfMultiplePasswords() throws IOException {
File key = writeFile("key.asc", KEY_1);
File wrong1 = writeFile("wrong_1", "BuzzAldr1n");
File correct = writeFile("correct", KEY_1_PASSWORD);
File wrong2 = writeFile("wrong_2", "NeilArmstr0ng");
pipeStringToStdin(MESSAGE);
ByteArrayOutputStream ciphertextOut = pipeStdoutToStream();
assertSuccess(executeCommand("inline-sign",
key.getAbsolutePath(),
"--with-key-password", wrong1.getAbsolutePath(),
"--with-key-password", correct.getAbsolutePath(),
"--with-key-password", wrong2.getAbsolutePath()));
File cert = writeFile("cert.asc", CERT_1);
pipeStringToStdin(ciphertextOut.toString());
ByteArrayOutputStream msgOut = pipeStdoutToStream();
File verificationsFile = nonExistentFile("verifications");
assertSuccess(executeCommand("inline-verify",
"--verifications-out", verificationsFile.getAbsolutePath(),
cert.getAbsolutePath()));
assertEquals(MESSAGE, msgOut.toString());
String verificationString = readStringFromFile(verificationsFile);
assertTrue(verificationString.contains(CERT_1_SIGNING_KEY));
}
@Test
public void cannotVerifyEncryptedMessage() throws IOException {
File key = writeFile("key.asc", KEY_2);
File cert = writeFile("cert.asc", CERT_2);
String msg = "Hello, World!\n";
pipeStringToStdin(msg);
ByteArrayOutputStream ciphertext = pipeStdoutToStream();
assertSuccess(executeCommand("encrypt", cert.getAbsolutePath(),
"--sign-with", key.getAbsolutePath()));
File verifications = nonExistentFile("verifications");
pipeBytesToStdin(ciphertext.toByteArray());
ByteArrayOutputStream out = pipeStdoutToStream();
int exitCode = executeCommand("inline-verify", cert.getAbsolutePath(),
"--verifications-out", verifications.getAbsolutePath());
assertEquals(SOPGPException.BadData.EXIT_CODE, exitCode);
assertEquals(0, out.size());
}
@Test
public void createMalformedMessage() throws IOException, PGPException {
String msg = "Hello, World!\n";
PGPSecretKeyRing key = PGPainless.readKeyRing().secretKeyRing(KEY_2);
ByteArrayOutputStream ciphertext = new ByteArrayOutputStream();
EncryptionStream encryptionStream = PGPainless.encryptAndOrSign()
.onOutputStream(ciphertext)
.withOptions(ProducerOptions.sign(SigningOptions.get()
.addDetachedSignature(SecretKeyRingProtector.unprotectedKeys(), key)
).overrideCompressionAlgorithm(CompressionAlgorithm.UNCOMPRESSED)
.setAsciiArmor(false));
encryptionStream.write(msg.getBytes(StandardCharsets.UTF_8));
encryptionStream.close();
PGPSignature sig = encryptionStream.getResult().getDetachedSignatures().entrySet()
.iterator().next().getValue().iterator().next();
ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out);
armorOut.write(ciphertext.toByteArray());
armorOut.write(sig.getEncoded());
armorOut.close();
}
@Test
public void cannotVerifyMalformedMessage() throws IOException {
// appended signature -> malformed
String malformedSignedMessage = "-----BEGIN PGP MESSAGE-----\n" +
"Version: BCPG v1.72b04\n" +
"\n" +
"yxRiAAAAAABIZWxsbywgV29ybGQhCoh1BAAWCgAnBQJjd52aCRCPvdNtAYMWcxYh\n" +
"BHoHPt8nPJAnltJZUo+9020BgxZzAACThwD/Vr7CMitMOul40VK12XXjOv5f8vgi\n" +
"ksqhrI2ysItID9oA/0Csgf3Sv2YenYVzqnd0hhiPe5IVPl8w4sTZKpriYMIG\n" +
"=DPPU\n" +
"-----END PGP MESSAGE-----";
File cert = writeFile("cert.asc", CERT_2);
File verifications = nonExistentFile("verifications");
pipeStringToStdin(malformedSignedMessage);
ByteArrayOutputStream out = pipeStdoutToStream();
int exitCode = executeCommand("inline-verify", cert.getAbsolutePath(),
"--verifications-out", verifications.getAbsolutePath());
assertEquals(SOPGPException.BadData.EXIT_CODE, exitCode);
assertEquals("Hello, World!\n", out.toString());
}
@Test
public void verifyPrependedSignedMessage() throws IOException {
// message with prepended signature
String malformedSignedMessage = "-----BEGIN PGP SIGNATURE-----\n" +
"Version: BCPG v1.72b04\n" +
"\n" +
"iHUEABYKACcFAmN3nOUJEI+9020BgxZzFiEEegc+3yc8kCeW0llSj73TbQGDFnMA\n" +
"ANPKAPkBxLVHvgeCkX/tTHdBH3CDeuUQF2wmtUmGXqhZA1IFtwD/dK0XQBHO3RO+\n" +
"GHpzA7fDAroqF0zM72tu2W4PPw04FgKjATstksQAAh6pOTn5Ogrh+UU5KYpcAA==\n" +
"=xtik\n" +
"-----END PGP SIGNATURE-----";
File cert = writeFile("cert.asc", CERT_2);
File verifications = nonExistentFile("verifications");
pipeStringToStdin(malformedSignedMessage);
ByteArrayOutputStream out = pipeStdoutToStream();
assertSuccess(executeCommand("inline-verify", cert.getAbsolutePath(),
"--verifications-out", verifications.getAbsolutePath()));
assertEquals("Hello, World!\n", out.toString());
String ver = readStringFromFile(verifications);
assertEquals(
"2022-11-18T14:55:33Z 7A073EDF273C902796D259528FBDD36D01831673 AEA0FD2C899D3FC077815F0026560D2AE53DB86F mode:binary\n", ver);
}
@Test
public void testInlineSignWithMissingSecretKeysFails() throws IOException {
String missingSecretKeys = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" +
"Comment: 8677 37CA 1979 28FA 325A DE56 B455 9329 9882 36BE\n" +
"Comment: Mrs. Secret Key <miss@secret.key>\n" +
"\n" +
"lEwEY3t3pRYJKwYBBAHaRw8BAQdA7lifUc85s7omw7eYNIaIj2mZrGeZ9KkG0WX2\n" +
"hAx5qXT+AGUAR05VAhAAAAAAAAAAAAAAAAAAAAAAtCFNcnMuIFNlY3JldCBLZXkg\n" +
"PG1pc3NAc2VjcmV0LmtleT6IjwQTFgoAQQUCY3t3pQkQtFWTKZiCNr4WIQSGdzfK\n" +
"GXko+jJa3la0VZMpmII2vgKeAQKbAQUWAgMBAAQLCQgHBRUKCQgLApkBAABNTQEA\n" +
"uU5L9hJ1QKWxL5wetJwR08rXJTzsuX1LRfy8dlnlJl0BAKPSqydLoTEVlJQ/2sjO\n" +
"xQmc6aedoOoXKKVNDW5ibrsEnFEEY3t3pRIKKwYBBAGXVQEFAQEHQA/WdwR+NFaY\n" +
"7NeZnRwI3X9sI5fMq0vtEauMLfZjqTc/AwEIB/4AZQBHTlUCEAAAAAAAAAAAAAAA\n" +
"AAAAAACIdQQYFgoAHQUCY3t3pQKeAQKbDAUWAgMBAAQLCQgHBRUKCQgLAAoJELRV\n" +
"kymYgja+8XMA/1quBVvaSf4QxbB2S7rKt93rAynDLqGQD8hC6wiZc+ihAQC87n2r\n" +
"meZ9kiYLYiQuBTGvXyzDBtw5m7wQtMWTfXisBpxMBGN7d6UWCSsGAQQB2kcPAQEH\n" +
"QMguDhFon0ZI//CIpC2ZndmtvKdJhcEAeVNkdcsIZajl/gBlAEdOVQIQAAAAAAAA\n" +
"AAAAAAAAAAAAAIjVBBgWCgB9BQJje3elAp4BApsCBRYCAwEABAsJCAcFFQoJCAtf\n" +
"IAQZFgoABgUCY3t3pQAKCRC14KclsvqqOstPAQDYiL7+4HucWKmd7dcd9XJZpdB6\n" +
"lneoK0qku0wvTVjX7gEAtUt2eXMlBE4ox+ZmY964PCc2gEHuC7PBtsAzuF7GSQwA\n" +
"CgkQtFWTKZiCNr7JKwEA3aLsOWAYzqvKgiboYSzle+SVBUb3chKlzf3YmckjmwgA\n" +
"/3YN1W8CiQFvE9NvetZkr2wXB+QVkuL6cxM0ogEo4lAG\n" +
"=9ZMl\n" +
"-----END PGP PRIVATE KEY BLOCK-----\n";
File key = writeFile("key.asc", missingSecretKeys);
pipeStringToStdin("Hello, World!\n");
ByteArrayOutputStream out = pipeStdoutToStream();
int exitCode = executeCommand("inline-sign", key.getAbsolutePath());
assertEquals(SOPGPException.KeyCannotSign.EXIT_CODE, exitCode);
assertEquals(0, out.size());
}
@Test
public void signWithProtectedKeyWithWrongPassphraseFails() throws IOException {
File key = writeFile("key.asc", KEY_1);
File password = writeFile("password.asc", "not_correct!");
pipeStringToStdin("Hello, World!\n");
ByteArrayOutputStream out = pipeStdoutToStream();
int exitCode = executeCommand("inline-sign", key.getAbsolutePath(),
"--with-key-password", password.getAbsolutePath());
assertEquals(SOPGPException.KeyIsProtected.EXIT_CODE, exitCode);
assertEquals(0, out.size());
}
}

View file

@ -1,52 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.cli.commands;
import static org.junit.jupiter.api.Assertions.assertFalse;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import org.junit.jupiter.api.Test;
import org.slf4j.LoggerFactory;
public class RoundTripInlineSignVerifyCmdTest extends CLITest {
public RoundTripInlineSignVerifyCmdTest() {
super(LoggerFactory.getLogger(RoundTripInlineSignVerifyCmdTest.class));
}
@Test
public void encryptAndDecryptAMessage() throws IOException {
// write password file
File password = writeFile("password", "sw0rdf1sh");
// generate key
File sigmundKey = pipeStdoutToFile("sigmund.key");
assertSuccess(executeCommand("generate-key", "--with-key-password=" + password.getAbsolutePath(),
"Sigmund Freud <sigmund@pgpainless.org>"));
// extract cert
File sigmundCert = pipeStdoutToFile("sigmund.cert");
pipeFileToStdin(sigmundKey);
assertSuccess(executeCommand("extract-cert"));
// sign message
pipeBytesToStdin("Hello, World!\n".getBytes(StandardCharsets.UTF_8));
File signedMsg = pipeStdoutToFile("signed.asc");
assertSuccess(executeCommand("inline-sign", "--with-key-password=" + password.getAbsolutePath(),
sigmundKey.getAbsolutePath()));
// verify message
File verifyFile = nonExistentFile("verify.txt");
pipeFileToStdin(signedMsg);
assertSuccess(executeCommand("inline-verify", "--verifications-out", verifyFile.getAbsolutePath(),
sigmundCert.getAbsolutePath()));
String verifications = readStringFromFile(verifyFile);
assertFalse(verifications.trim().isEmpty());
}
}

View file

@ -13,7 +13,6 @@ import java.io.File;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException;
import java.text.ParseException;
import java.util.Date;
import org.bouncycastle.openpgp.PGPException;
@ -25,8 +24,8 @@ import org.pgpainless.algorithm.KeyFlag;
import org.pgpainless.key.OpenPgpV4Fingerprint;
import org.pgpainless.key.generation.KeySpec;
import org.pgpainless.key.generation.type.KeyType;
import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve;
import org.pgpainless.key.generation.type.xdh_legacy.XDHLegacySpec;
import org.pgpainless.key.generation.type.eddsa.EdDSACurve;
import org.pgpainless.key.generation.type.xdh.XDHSpec;
import org.pgpainless.key.info.KeyRingInfo;
import org.slf4j.LoggerFactory;
import sop.exception.SOPGPException;
@ -95,7 +94,7 @@ public class RoundTripSignVerifyCmdTest extends CLITest {
"=VWAZ\n" +
"-----END PGP SIGNATURE-----";
private static final String BINARY_SIG_VERIFICATION =
"2022-11-09T18:40:24Z 444C10AB011EF8424C83F0A9DA9F413986211DC6 9DA09423C9F94BA4CCA30951099B11BF296A373E mode:binary\n";
"2022-11-09T18:40:24Z 444C10AB011EF8424C83F0A9DA9F413986211DC6 9DA09423C9F94BA4CCA30951099B11BF296A373E\n";
private static final String TEXT_SIG = "-----BEGIN PGP SIGNATURE-----\n" +
"Version: PGPainless\n" +
"\n" +
@ -105,16 +104,8 @@ public class RoundTripSignVerifyCmdTest extends CLITest {
"=s5xn\n" +
"-----END PGP SIGNATURE-----";
private static final String TEXT_SIG_VERIFICATION =
"2022-11-09T18:41:18Z 444C10AB011EF8424C83F0A9DA9F413986211DC6 9DA09423C9F94BA4CCA30951099B11BF296A373E mode:text\n";
private static final Date TEXT_SIG_CREATION;
static {
try {
TEXT_SIG_CREATION = UTCUtil.parseUTCDate("2022-11-09T18:41:18Z");
} catch (ParseException e) {
throw new RuntimeException(e);
}
}
"2022-11-09T18:41:18Z 444C10AB011EF8424C83F0A9DA9F413986211DC6 9DA09423C9F94BA4CCA30951099B11BF296A373E\n";
private static final Date TEXT_SIG_CREATION = UTCUtil.parseUTCDate("2022-11-09T18:41:18Z");
@Test
public void createArmoredSignature() throws IOException {
@ -198,12 +189,11 @@ public class RoundTripSignVerifyCmdTest extends CLITest {
}
@Test
public void testSignWithIncapableKey()
throws IOException, PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException {
public void testSignWithIncapableKey() throws IOException, PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException {
PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing()
.addUserId("Cannot Sign <cannot@sign.key>")
.setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER))
.addSubkey(KeySpec.getBuilder(KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE))
.setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER))
.addSubkey(KeySpec.getBuilder(KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE))
.build();
File keyFile = writeFile("key.pgp", secretKeys.getEncoded());

View file

@ -29,7 +29,7 @@ public class VersionCmdTest extends CLITest {
public void testGetBackendVersion() throws IOException {
ByteArrayOutputStream out = pipeStdoutToStream();
assertSuccess(executeCommand("version", "--backend"));
assertTrue(out.toString().startsWith("PGPainless "));
assertTrue(out.toString().startsWith("Bouncy Castle "));
}
@Test
@ -41,12 +41,4 @@ public class VersionCmdTest extends CLITest {
assertTrue(info.contains("Bouncy Castle"));
assertTrue(info.contains("Stateless OpenPGP Protocol"));
}
@Test
public void testSopSpecVersion() throws IOException {
ByteArrayOutputStream out = pipeStdoutToStream();
assertSuccess(executeCommand("version", "--sop-spec"));
String info = out.toString();
assertTrue(info.startsWith("draft-dkg-openpgp-stateless-cli-"));
}
}

View file

@ -4,18 +4,28 @@
package org.pgpainless.cli.misc;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import com.ginsberg.junit.exit.ExpectSystemExitWithStatus;
import org.bouncycastle.util.io.Streams;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.pgpainless.cli.commands.CLITest;
import org.slf4j.LoggerFactory;
import org.pgpainless.cli.PGPainlessCLI;
import org.pgpainless.cli.TestUtils;
import sop.exception.SOPGPException;
public class SignUsingPublicKeyBehaviorTest extends CLITest {
public class SignUsingPublicKeyBehaviorTest {
public static final String KEY_THAT_IS_A_CERT = "" +
"-----BEGIN PGP PUBLIC KEY BLOCK-----\n" +
@ -79,24 +89,61 @@ public class SignUsingPublicKeyBehaviorTest extends CLITest {
"=oJQ2\n" +
"-----END PGP PUBLIC KEY BLOCK-----";
public SignUsingPublicKeyBehaviorTest() {
super(LoggerFactory.getLogger(SignUsingPublicKeyBehaviorTest.class));
private static File tempDir;
private static PrintStream originalSout;
@BeforeAll
public static void prepare() throws IOException {
tempDir = TestUtils.createTempDirectory();
}
@Test
@ExpectSystemExitWithStatus(SOPGPException.KeyCannotSign.EXIT_CODE)
public void testSignatureCreationAndVerification() throws IOException {
originalSout = System.out;
InputStream originalIn = System.in;
// Write alice key to disc
File aliceKeyFile = writeFile("alice.key", KEY_THAT_IS_A_CERT);
File aliceKeyFile = new File(tempDir, "alice.key");
assertTrue(aliceKeyFile.createNewFile());
OutputStream aliceKeyOut = new FileOutputStream(aliceKeyFile);
Streams.pipeAll(new ByteArrayInputStream(KEY_THAT_IS_A_CERT.getBytes(StandardCharsets.UTF_8)), aliceKeyOut);
aliceKeyOut.close();
// Write alice pub key to disc
File aliceCertFile = new File(tempDir, "alice.pub");
assertTrue(aliceCertFile.createNewFile());
OutputStream aliceCertOut = new FileOutputStream(aliceCertFile);
Streams.pipeAll(new ByteArrayInputStream(KEY_THAT_IS_A_CERT.getBytes(StandardCharsets.UTF_8)), aliceCertOut);
aliceCertOut.close();
// Write test data to disc
File dataFile = writeFile("data", "If privacy is outlawed, only outlaws will have privacy.\n");
String data = "If privacy is outlawed, only outlaws will have privacy.\n";
File dataFile = new File(tempDir, "data");
assertTrue(dataFile.createNewFile());
FileOutputStream dataOut = new FileOutputStream(dataFile);
Streams.pipeAll(new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8)), dataOut);
dataOut.close();
// Sign test data
File sigFile = pipeStdoutToFile("sig.asc");
pipeFileToStdin(dataFile);
assertEquals(SOPGPException.KeyCannotSign.EXIT_CODE,
executeCommand("sign", "--armor", aliceKeyFile.getAbsolutePath()));
FileInputStream dataIn = new FileInputStream(dataFile);
System.setIn(dataIn);
File sigFile = new File(tempDir, "sig.asc");
assertTrue(sigFile.createNewFile());
FileOutputStream sigOut = new FileOutputStream(sigFile);
System.setOut(new PrintStream(sigOut));
PGPainlessCLI.main(new String[] {"sign", "--armor", aliceKeyFile.getAbsolutePath()});
assertTrue(readStringFromFile(sigFile).trim().isEmpty());
System.setIn(originalIn);
}
@AfterAll
public static void after() {
System.setOut(originalSout);
// CHECKSTYLE:OFF
System.out.println(tempDir.getAbsolutePath());
// CHECKSTYLE:ON
}
}

View file

@ -20,18 +20,10 @@ dependencies {
testImplementation "ch.qos.logback:logback-classic:$logbackVersion"
// Bouncy Castle
api "org.bouncycastle:bcprov-jdk18on:$bouncyCastleVersion"
api "org.bouncycastle:bcpg-jdk18on:$bouncyPgVersion"
api "org.bouncycastle:bcutil-jdk18on:$bouncyCastleVersion"
api "org.bouncycastle:bcprov-jdk15to18:$bouncyCastleVersion"
api "org.bouncycastle:bcpg-jdk15to18:$bouncyPgVersion"
// api(files("../libs/bcpg-jdk18on-1.70.jar"))
// @Nullable, @Nonnull annotations
implementation "com.google.code.findbugs:jsr305:3.0.2"
}
// https://docs.gradle.org/current/userguide/java_library_plugin.html#sec:java_library_modular_auto
tasks.named('jar') {
manifest {
attributes('Automatic-Module-Name': 'org.pgpainless.core')
}
}

View file

@ -1,31 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.gnupg;
import org.bouncycastle.bcpg.S2K;
public enum GnuPGDummyExtension {
/**
* Do not store the secret part at all.
*/
NO_PRIVATE_KEY(S2K.GNU_PROTECTION_MODE_NO_PRIVATE_KEY),
/**
* A stub to access smartcards.
*/
DIVERT_TO_CARD(S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD),
;
private final int id;
GnuPGDummyExtension(int id) {
this.id = id;
}
public int getId() {
return id;
}
}

View file

@ -1,210 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.gnupg;
import org.bouncycastle.bcpg.PublicKeyPacket;
import org.bouncycastle.bcpg.S2K;
import org.bouncycastle.bcpg.SecretKeyPacket;
import org.bouncycastle.bcpg.SecretSubkeyPacket;
import org.bouncycastle.openpgp.PGPSecretKey;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.pgpainless.key.SubkeyIdentifier;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* This class can be used to remove private keys from secret software-keys by replacing them with
* stub secret keys in the style of GnuPGs proprietary extensions.
*
* @see <a href="https://git.gnupg.org/cgi-bin/gitweb.cgi?p=gnupg.git;a=blob;f=doc/DETAILS;hb=HEAD#l1489">
* GnuPGs doc/DETAILS - GNU extensions to the S2K algorithm</a>
*/
public final class GnuPGDummyKeyUtil {
private GnuPGDummyKeyUtil() {
}
/**
* Return the key-ids of all keys which appear to be stored on a hardware token / smartcard by GnuPG.
* Note, that this functionality is based on GnuPGs proprietary S2K extensions, which are not strictly required
* for dealing with hardware-backed keys.
*
* @param secretKeys secret keys
* @return set of keys with S2K type GNU_DUMMY_S2K and protection mode DIVERT_TO_CARD
*/
public static Set<SubkeyIdentifier> getIdsOfKeysWithGnuPGS2KDivertedToCard(@Nonnull PGPSecretKeyRing secretKeys) {
Set<SubkeyIdentifier> hardwareBackedKeys = new HashSet<>();
for (PGPSecretKey secretKey : secretKeys) {
S2K s2K = secretKey.getS2K();
if (s2K == null) {
continue;
}
int type = s2K.getType();
int mode = s2K.getProtectionMode();
// TODO: Is GNU_DUMMY_S2K appropriate?
if (type == S2K.GNU_DUMMY_S2K && mode == S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD) {
SubkeyIdentifier hardwareBackedKey = new SubkeyIdentifier(secretKeys, secretKey.getKeyID());
hardwareBackedKeys.add(hardwareBackedKey);
}
}
return hardwareBackedKeys;
}
/**
* Modify the given {@link PGPSecretKeyRing}.
*
* @param secretKeys secret keys
* @return builder
*/
public static Builder modify(@Nonnull PGPSecretKeyRing secretKeys) {
return new Builder(secretKeys);
}
public static final class Builder {
private final PGPSecretKeyRing keys;
private Builder(@Nonnull PGPSecretKeyRing keys) {
this.keys = keys;
}
/**
* Remove all private keys that match the given {@link KeyFilter} from the key ring and replace them with
* GNU_DUMMY keys with S2K protection mode {@link GnuPGDummyExtension#NO_PRIVATE_KEY}.
*
* @param filter filter to select keys for removal
* @return modified key ring
*/
public PGPSecretKeyRing removePrivateKeys(@Nonnull KeyFilter filter) {
return replacePrivateKeys(GnuPGDummyExtension.NO_PRIVATE_KEY, null, filter);
}
/**
* Remove all private keys that match the given {@link KeyFilter} from the key ring and replace them with
* GNU_DUMMY keys with S2K protection mode {@link GnuPGDummyExtension#DIVERT_TO_CARD}.
* This method will set the serial number of the card to 0x00000000000000000000000000000000.
* NOTE: This method does not actually move any keys to a card.
*
* @param filter filter to select keys for removal
* @return modified key ring
*/
public PGPSecretKeyRing divertPrivateKeysToCard(@Nonnull KeyFilter filter) {
return divertPrivateKeysToCard(filter, new byte[16]);
}
/**
* Remove all private keys that match the given {@link KeyFilter} from the key ring and replace them with
* GNU_DUMMY keys with S2K protection mode {@link GnuPGDummyExtension#DIVERT_TO_CARD}.
* This method will include the card serial number into the encoded dummy key.
* NOTE: This method does not actually move any keys to a card.
*
* @param filter filter to select keys for removal
* @param cardSerialNumber serial number of the card (at most 16 bytes long)
* @return modified key ring
*/
public PGPSecretKeyRing divertPrivateKeysToCard(@Nonnull KeyFilter filter, @Nullable byte[] cardSerialNumber) {
if (cardSerialNumber != null && cardSerialNumber.length > 16) {
throw new IllegalArgumentException("Card serial number length cannot exceed 16 bytes.");
}
return replacePrivateKeys(GnuPGDummyExtension.DIVERT_TO_CARD, cardSerialNumber, filter);
}
private PGPSecretKeyRing replacePrivateKeys(@Nonnull GnuPGDummyExtension extension,
@Nullable byte[] serial,
@Nonnull KeyFilter filter) {
byte[] encodedSerial = serial != null ? encodeSerial(serial) : null;
S2K s2k = extensionToS2K(extension);
List<PGPSecretKey> secretKeyList = new ArrayList<>();
for (PGPSecretKey secretKey : keys) {
if (!filter.filter(secretKey.getKeyID())) {
// No conversion, do not modify subkey
secretKeyList.add(secretKey);
continue;
}
PublicKeyPacket publicKeyPacket = secretKey.getPublicKey().getPublicKeyPacket();
if (secretKey.isMasterKey()) {
SecretKeyPacket keyPacket = new SecretKeyPacket(publicKeyPacket,
0, SecretKeyPacket.USAGE_SHA1, s2k, null, encodedSerial);
PGPSecretKey onCard = new PGPSecretKey(keyPacket, secretKey.getPublicKey());
secretKeyList.add(onCard);
} else {
SecretSubkeyPacket keyPacket = new SecretSubkeyPacket(publicKeyPacket,
0, SecretKeyPacket.USAGE_SHA1, s2k, null, encodedSerial);
PGPSecretKey onCard = new PGPSecretKey(keyPacket, secretKey.getPublicKey());
secretKeyList.add(onCard);
}
}
return new PGPSecretKeyRing(secretKeyList);
}
private byte[] encodeSerial(@Nonnull byte[] serial) {
byte[] encoded = new byte[serial.length + 1];
encoded[0] = (byte) (serial.length & 0xff);
System.arraycopy(serial, 0, encoded, 1, serial.length);
return encoded;
}
private S2K extensionToS2K(@Nonnull GnuPGDummyExtension extension) {
return S2K.gnuDummyS2K(extension == GnuPGDummyExtension.DIVERT_TO_CARD ?
S2K.GNUDummyParams.divertToCard() : S2K.GNUDummyParams.noPrivateKey());
}
}
/**
* Filter for selecting keys.
*/
@FunctionalInterface
public interface KeyFilter {
/**
* Return true, if the given key should be selected, false otherwise.
*
* @param keyId id of the key
* @return select
*/
boolean filter(long keyId);
/**
* Select any key.
*
* @return filter
*/
static KeyFilter any() {
return keyId -> true;
}
/**
* Select only the given keyId.
*
* @param onlyKeyId only acceptable key id
* @return filter
*/
static KeyFilter only(long onlyKeyId) {
return keyId -> keyId == onlyKeyId;
}
/**
* Select all keyIds which are contained in the given set of ids.
*
* @param ids set of acceptable keyIds
* @return filter
*/
static KeyFilter selected(Collection<Long> ids) {
// noinspection Convert2MethodRef
return keyId -> ids.contains(keyId);
}
}
}

View file

@ -0,0 +1,222 @@
// 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
*/
public static KeyRingTemplates generateKeyRing() {
return new KeyRingTemplates();
}
/**
* Build a custom OpenPGP key ring.
*
* @return builder
*/
public static KeyRingBuilder buildKeyRing() {
return new KeyRingBuilder();
}
/**
* Read an existing OpenPGP key ring.
* @return builder
*/
public static KeyRingReader readKeyRing() {
return new KeyRingReader();
}
/**
* Extract a public key certificate from a secret key.
*
* @param secretKey secret key
* @return public key certificate
*/
public static PGPPublicKeyRing extractCertificate(@Nonnull PGPSecretKeyRing secretKey) {
return KeyRingUtils.publicKeyRingFrom(secretKey);
}
/**
* Merge two copies of the same certificate (e.g. an old copy, and one retrieved from a key server) together.
*
* @param originalCopy local, older copy of the cert
* @param updatedCopy updated, newer copy of the cert
* @return merged certificate
* @throws PGPException in case of an error
*/
public static PGPPublicKeyRing mergeCertificate(
@Nonnull PGPPublicKeyRing originalCopy,
@Nonnull PGPPublicKeyRing updatedCopy)
throws PGPException {
return PGPPublicKeyRing.join(originalCopy, updatedCopy);
}
/**
* Wrap a key or certificate in ASCII armor.
*
* @param key key or certificate
* @return ascii armored string
*
* @throws IOException in case of an error in the {@link ArmoredOutputStream}
*/
public static String asciiArmor(@Nonnull PGPKeyRing key)
throws IOException {
if (key instanceof PGPSecretKeyRing) {
return ArmorUtils.toAsciiArmoredString((PGPSecretKeyRing) key);
} else {
return ArmorUtils.toAsciiArmoredString((PGPPublicKeyRing) key);
}
}
/**
* Wrap the detached signature in ASCII armor.
*
* @param signature detached signature
* @return ascii armored string
*
* @throws IOException in case of an error in the {@link ArmoredOutputStream}
*/
public static String asciiArmor(@Nonnull PGPSignature signature)
throws IOException {
return ArmorUtils.toAsciiArmoredString(signature);
}
/**
* Wrap a key of certificate in ASCII armor and write the result into the given {@link OutputStream}.
*
* @param key key or certificate
* @param outputStream output stream
*
* @throws IOException in case of an error ion the {@link ArmoredOutputStream}
*/
public static void asciiArmor(@Nonnull PGPKeyRing key, @Nonnull OutputStream outputStream)
throws IOException {
ArmoredOutputStream armorOut = ArmorUtils.toAsciiArmoredStream(key, outputStream);
key.encode(armorOut);
armorOut.close();
}
/**
* Create an {@link EncryptionStream}, which can be used to encrypt and/or sign data using OpenPGP.
*
* @return builder
*/
public static EncryptionBuilder encryptAndOrSign() {
return new EncryptionBuilder();
}
/**
* Create a {@link DecryptionStream}, which can be used to decrypt and/or verify data using OpenPGP.
*
* @return builder
*/
public static DecryptionBuilder decryptAndOrVerify() {
return new DecryptionBuilder();
}
/**
* Make changes to a secret key.
* This method can be used to change key expiration dates and passphrases, or add/revoke subkeys.
*
* After making the desired changes in the builder, the modified key ring can be extracted using {@link SecretKeyRingEditorInterface#done()}.
*
* @param secretKeys secret key ring
* @return builder
*/
public static SecretKeyRingEditorInterface modifyKeyRing(PGPSecretKeyRing secretKeys) {
return modifyKeyRing(secretKeys, null);
}
/**
* Make changes to a secret key at the given reference time.
* This method can be used to change key expiration dates and passphrases, or add/revoke user-ids and subkeys.
*
* After making the desired changes in the builder, the modified key can be extracted using {@link SecretKeyRingEditorInterface#done()}.
*
* @param secretKeys secret key ring
* @param referenceTime reference time used as signature creation date
* @return builder
*/
public static SecretKeyRingEditorInterface modifyKeyRing(PGPSecretKeyRing secretKeys, Date referenceTime) {
return new SecretKeyRingEditor(secretKeys, referenceTime);
}
/**
* Quickly access information about a {@link org.bouncycastle.openpgp.PGPPublicKeyRing} / {@link PGPSecretKeyRing}.
* This method can be used to determine expiration dates, key flags and other information about a key.
*
* To evaluate a key at a given date (e.g. to determine if the key was allowed to create a certain signature)
* use {@link #inspectKeyRing(PGPKeyRing, Date)} instead.
*
* @param keyRing key ring
* @return access object
*/
public static KeyRingInfo inspectKeyRing(PGPKeyRing keyRing) {
return new KeyRingInfo(keyRing);
}
/**
* Quickly access information about a {@link org.bouncycastle.openpgp.PGPPublicKeyRing} / {@link PGPSecretKeyRing}.
* This method can be used to determine expiration dates, key flags and other information about a key at a specific time.
*
* @param keyRing key ring
* @param referenceTime date of inspection
* @return access object
*/
public static KeyRingInfo inspectKeyRing(PGPKeyRing keyRing, Date referenceTime) {
return new KeyRingInfo(keyRing, referenceTime);
}
/**
* Access, and make changes to PGPainless policy on acceptable/default algorithms etc.
*
* @return policy
*/
public static Policy getPolicy() {
return Policy.getInstance();
}
/**
* Create different kinds of signatures on other keys.
*
* @return builder
*/
public static CertifyCertificate certify() {
return new CertifyCertificate();
}
}

View file

@ -0,0 +1,95 @@
// 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;
}
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;
}
}

View file

@ -0,0 +1,63 @@
// 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;
}
}

View file

@ -0,0 +1,46 @@
// 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;
}
}

View file

@ -0,0 +1,79 @@
// 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;
}
}

View file

@ -0,0 +1,35 @@
// 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 &lt;CR&gt;&lt;LF&gt;}
* </pre>.
*/
CANONICAL_TEXT_DOCUMENT(SignatureType.CANONICAL_TEXT_DOCUMENT),
;
final SignatureType signatureType;
DocumentSignatureType(SignatureType signatureType) {
this.signatureType = signatureType;
}
public SignatureType getSignatureType() {
return signatureType;
}
}

View file

@ -0,0 +1,22 @@
// 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
}

View file

@ -0,0 +1,152 @@
// 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>
*/
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>
*/
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;
}
}

View file

@ -0,0 +1,118 @@
// 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;
}
}

View file

@ -0,0 +1,121 @@
// 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 other keys.
*/
CERTIFY_OTHER (KeyFlags.CERTIFY_OTHER),
/**
* This key may be used to sign data.
*/
SIGN_DATA (KeyFlags.SIGN_DATA),
/**
* This key may be used to encrypt communications.
*/
ENCRYPT_COMMS (KeyFlags.ENCRYPT_COMMS),
/**
* This key may be used to encrypt storage.
*/
ENCRYPT_STORAGE(KeyFlags.ENCRYPT_STORAGE),
/**
* The private component of this key may have been split by a secret-sharing mechanism.
*/
SPLIT (KeyFlags.SPLIT),
/**
* This key may be used for authentication.
*/
AUTHENTICATION (KeyFlags.AUTHENTICATION),
/**
* The private component of this key may be in the possession of more than one person.
*/
SHARED (KeyFlags.SHARED),
;
private final int flag;
KeyFlag(int flag) {
this.flag = flag;
}
/**
* Return the numeric id of the {@link KeyFlag}.
*
* @return numeric id
*/
public int getFlag() {
return flag;
}
/**
* Convert a bitmask into a list of {@link KeyFlag KeyFlags}.
*
* @param bitmask bitmask
* @return list of key flags encoded by the bitmask
*/
public static List<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;
}
}

View file

@ -0,0 +1,163 @@
// 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 https://tools.ietf.org/html/rfc4880#section-13.5
*/
@Deprecated
RSA_ENCRYPT (PublicKeyAlgorithmTags.RSA_ENCRYPT, false, true),
/**
* RSA with usage of creating signatures.
*
* @deprecated see https://tools.ietf.org/html/rfc4880#section-13.5
*/
@Deprecated
RSA_SIGN (PublicKeyAlgorithmTags.RSA_SIGN, true, false),
/**
* ElGamal with usage encryption.
*/
ELGAMAL_ENCRYPT (PublicKeyAlgorithmTags.ELGAMAL_ENCRYPT, false, true),
/**
* Digital Signature Algorithm.
*/
DSA (PublicKeyAlgorithmTags.DSA, true, false),
/**
* EC is deprecated.
* @deprecated use {@link #ECDH} instead.
*/
@Deprecated
EC (PublicKeyAlgorithmTags.EC, false, true),
/**
* Elliptic Curve Diffie-Hellman.
*/
ECDH (PublicKeyAlgorithmTags.ECDH, false, true),
/**
* Elliptic Curve Digital Signature Algorithm.
*/
ECDSA (PublicKeyAlgorithmTags.ECDSA, true, false),
/**
* ElGamal General.
*
* @deprecated see https://tools.ietf.org/html/rfc4880#section-13.8
*/
@Deprecated
ELGAMAL_GENERAL (PublicKeyAlgorithmTags.ELGAMAL_GENERAL, true, true),
/**
* Diffie-Hellman key exchange algorithm.
*/
DIFFIE_HELLMAN (PublicKeyAlgorithmTags.DIFFIE_HELLMAN, false, true),
/**
* Digital Signature Algorithm based on twisted Edwards Curves.
*/
EDDSA (PublicKeyAlgorithmTags.EDDSA, true, false),
;
private static final Map<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;
}
}

View file

@ -0,0 +1,131 @@
// 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;
}
}

View file

@ -0,0 +1,23 @@
// 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
}

View file

@ -0,0 +1,462 @@
// 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;
}
}

View file

@ -0,0 +1,225 @@
// 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);
}
}
}

View file

@ -0,0 +1,101 @@
// 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;
}
}

View file

@ -0,0 +1,150 @@
// 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;
}
}

View file

@ -0,0 +1,188 @@
// 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;
}
}

View file

@ -0,0 +1,70 @@
// 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();
}
};
}
}

View file

@ -0,0 +1,105 @@
// 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();
}
};
}
}

View file

@ -0,0 +1,8 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
/**
* Classes related to algorithm negotiation.
*/
package org.pgpainless.algorithm.negotiation;

View file

@ -0,0 +1,8 @@
// 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;

View file

@ -0,0 +1,39 @@
// 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;
public abstract class CloseForResultInputStream extends InputStream {
protected final OpenPgpMetadata.Builder resultBuilder;
private boolean isClosed = false;
public CloseForResultInputStream(@Nonnull OpenPgpMetadata.Builder resultBuilder) {
this.resultBuilder = resultBuilder;
}
@Override
public void close() throws IOException {
this.isClosed = true;
}
/**
* Return the result of the decryption.
* The result contains metadata about the decryption, such as signatures, used keys and algorithms, as well as information
* about the decrypted file/stream.
*
* Can only be obtained once the stream got successfully closed ({@link #close()}).
* @return metadata
*/
public OpenPgpMetadata getResult() {
if (!isClosed) {
throw new IllegalStateException("Stream MUST be closed before the result can be accessed.");
}
return resultBuilder.build();
}
}

View file

@ -0,0 +1,368 @@
// 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.pgpainless.decryption_verification.cleartext_signatures.InMemoryMultiPassStrategy;
import org.pgpainless.decryption_verification.cleartext_signatures.MultiPassStrategy;
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();
// Set of verification keys
private final Set<PGPPublicKeyRing> certificates = new HashSet<>();
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<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.add(verificationCert);
return this;
}
/**
* Add a set of certificates (public key rings) for signature verification.
*
* @param verificationCerts certificates for signature verification
* @return options
*/
public ConsumerOptions addVerificationCerts(PGPPublicKeyRingCollection verificationCerts) {
for (PGPPublicKeyRing certificate : verificationCerts) {
addVerificationCert(certificate);
}
return this;
}
public ConsumerOptions addVerificationOfDetachedSignatures(InputStream signatureInputStream) throws IOException, PGPException {
List<PGPSignature> signatures = SignatureUtils.readSignatures(signatureInputStream);
return addVerificationOfDetachedSignatures(signatures);
}
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;
}
public @Nonnull Set<PGPSecretKeyRing> getDecryptionKeys() {
return Collections.unmodifiableSet(decryptionKeys.keySet());
}
public @Nonnull Set<Passphrase> getDecryptionPassphrases() {
return Collections.unmodifiableSet(decryptionPassphrases);
}
public @Nonnull Set<PGPPublicKeyRing> getCertificates() {
return Collections.unmodifiableSet(certificates);
}
public @Nullable MissingPublicKeyCallback getMissingCertificateCallback() {
return missingCertificateCallback;
}
public @Nonnull SecretKeyRingProtector getSecretKeyProtector(PGPSecretKeyRing decryptionKeyRing) {
return decryptionKeys.get(decryptionKeyRing);
}
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;
}
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;
}
}

View file

@ -0,0 +1,37 @@
// 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 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 DecryptionStreamFactory.create(inputStream, consumerOptions);
}
}
}

View file

@ -0,0 +1,36 @@
// 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;
}
}

View file

@ -0,0 +1,65 @@
// 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.util.io.Streams;
/**
* Decryption Stream that handles updating and verification of detached signatures,
* as well as verification of integrity-protected input streams once the stream gets closed.
*/
public class DecryptionStream extends CloseForResultInputStream {
private final InputStream inputStream;
private final IntegrityProtectedInputStream integrityProtectedInputStream;
private final InputStream armorStream;
/**
* Create an input stream that handles decryption and - if necessary - integrity protection verification.
*
* @param wrapped underlying input stream
* @param resultBuilder builder for decryption metadata like algorithms, recipients etc.
* @param integrityProtectedInputStream in case of data encrypted using SEIP packet close this stream to check integrity
* @param armorStream armor stream to verify CRC checksums
*/
DecryptionStream(@Nonnull InputStream wrapped,
@Nonnull OpenPgpMetadata.Builder resultBuilder,
IntegrityProtectedInputStream integrityProtectedInputStream,
InputStream armorStream) {
super(resultBuilder);
this.inputStream = wrapped;
this.integrityProtectedInputStream = integrityProtectedInputStream;
this.armorStream = armorStream;
}
@Override
public void close() throws IOException {
if (armorStream != null) {
Streams.drain(armorStream);
}
inputStream.close();
if (integrityProtectedInputStream != null) {
integrityProtectedInputStream.close();
}
super.close();
}
@Override
public int read() throws IOException {
int r = inputStream.read();
return r;
}
@Override
public int read(@Nonnull byte[] bytes, int offset, int length) throws IOException {
int read = inputStream.read(bytes, offset, length);
return read;
}
}

View file

@ -0,0 +1,650 @@
// 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 java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.bouncycastle.bcpg.ArmoredInputStream;
import org.bouncycastle.openpgp.PGPCompressedData;
import org.bouncycastle.openpgp.PGPEncryptedData;
import org.bouncycastle.openpgp.PGPEncryptedDataList;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPLiteralData;
import org.bouncycastle.openpgp.PGPObjectFactory;
import org.bouncycastle.openpgp.PGPOnePassSignature;
import org.bouncycastle.openpgp.PGPOnePassSignatureList;
import org.bouncycastle.openpgp.PGPPBEEncryptedData;
import org.bouncycastle.openpgp.PGPPrivateKey;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPSecretKey;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.openpgp.PGPSessionKey;
import org.bouncycastle.openpgp.PGPSignature;
import org.bouncycastle.openpgp.PGPSignatureList;
import org.bouncycastle.openpgp.PGPUtil;
import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory;
import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider;
import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory;
import org.bouncycastle.openpgp.operator.SessionKeyDataDecryptorFactory;
import org.pgpainless.PGPainless;
import org.pgpainless.algorithm.CompressionAlgorithm;
import org.pgpainless.algorithm.EncryptionPurpose;
import org.pgpainless.algorithm.StreamEncoding;
import org.pgpainless.algorithm.SymmetricKeyAlgorithm;
import org.pgpainless.decryption_verification.cleartext_signatures.ClearsignedMessageUtil;
import org.pgpainless.decryption_verification.cleartext_signatures.MultiPassStrategy;
import org.pgpainless.exception.FinalIOException;
import org.pgpainless.exception.MessageNotIntegrityProtectedException;
import org.pgpainless.exception.MissingDecryptionMethodException;
import org.pgpainless.exception.MissingLiteralDataException;
import org.pgpainless.exception.MissingPassphraseException;
import org.pgpainless.exception.SignatureValidationException;
import org.pgpainless.exception.UnacceptableAlgorithmException;
import org.pgpainless.implementation.ImplementationFactory;
import org.pgpainless.key.SubkeyIdentifier;
import org.pgpainless.key.info.KeyRingInfo;
import org.pgpainless.key.protection.SecretKeyRingProtector;
import org.pgpainless.key.protection.UnlockSecretKey;
import org.pgpainless.signature.SignatureUtils;
import org.pgpainless.signature.consumer.DetachedSignatureCheck;
import org.pgpainless.signature.consumer.OnePassSignatureCheck;
import org.pgpainless.util.ArmoredInputStreamFactory;
import org.pgpainless.util.Passphrase;
import org.pgpainless.util.SessionKey;
import org.pgpainless.util.Tuple;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public final class DecryptionStreamFactory {
private static final Logger LOGGER = LoggerFactory.getLogger(DecryptionStreamFactory.class);
// Maximum nesting depth of packets (e.g. compression, encryption...)
private static final int MAX_PACKET_NESTING_DEPTH = 16;
private final ConsumerOptions options;
private final OpenPgpMetadata.Builder resultBuilder = OpenPgpMetadata.getBuilder();
private final List<OnePassSignatureCheck> onePassSignatureChecks = new ArrayList<>();
private final List<DetachedSignatureCheck> detachedSignatureChecks = new ArrayList<>();
private final Map<Long, OnePassSignatureCheck> onePassSignaturesWithMissingCert = new HashMap<>();
private static final PGPContentVerifierBuilderProvider verifierBuilderProvider =
ImplementationFactory.getInstance().getPGPContentVerifierBuilderProvider();
private IntegrityProtectedInputStream integrityProtectedEncryptedInputStream;
public static DecryptionStream create(@Nonnull InputStream inputStream,
@Nonnull ConsumerOptions options)
throws PGPException, IOException {
DecryptionStreamFactory factory = new DecryptionStreamFactory(options);
OpenPgpInputStream openPgpIn = new OpenPgpInputStream(inputStream);
return factory.parseOpenPGPDataAndCreateDecryptionStream(openPgpIn);
}
public DecryptionStreamFactory(ConsumerOptions options) {
this.options = options;
initializeDetachedSignatures(options.getDetachedSignatures());
}
private void initializeDetachedSignatures(Set<PGPSignature> signatures) {
for (PGPSignature signature : signatures) {
long issuerKeyId = SignatureUtils.determineIssuerKeyId(signature);
PGPPublicKeyRing signingKeyRing = findSignatureVerificationKeyRing(issuerKeyId);
if (signingKeyRing == null) {
SignatureValidationException ex = new SignatureValidationException(
"Missing verification certificate " + Long.toHexString(issuerKeyId));
resultBuilder.addInvalidDetachedSignature(new SignatureVerification(signature, null), ex);
continue;
}
PGPPublicKey signingKey = signingKeyRing.getPublicKey(issuerKeyId);
SubkeyIdentifier signingKeyIdentifier = new SubkeyIdentifier(signingKeyRing, signingKey.getKeyID());
try {
signature.init(verifierBuilderProvider, signingKey);
DetachedSignatureCheck detachedSignature =
new DetachedSignatureCheck(signature, signingKeyRing, signingKeyIdentifier);
detachedSignatureChecks.add(detachedSignature);
} catch (PGPException e) {
SignatureValidationException ex = new SignatureValidationException(
"Cannot verify detached signature made by " + signingKeyIdentifier + ".", e);
resultBuilder.addInvalidDetachedSignature(new SignatureVerification(signature, signingKeyIdentifier), ex);
}
}
}
private DecryptionStream parseOpenPGPDataAndCreateDecryptionStream(OpenPgpInputStream openPgpIn)
throws IOException, PGPException {
InputStream pgpInStream;
InputStream outerDecodingStream;
PGPObjectFactory objectFactory;
// Non-OpenPGP data. We are probably verifying detached signatures
if (openPgpIn.isNonOpenPgp() || options.isForceNonOpenPgpData()) {
outerDecodingStream = openPgpIn;
pgpInStream = wrapInVerifySignatureStream(outerDecodingStream, null);
return new DecryptionStream(pgpInStream, resultBuilder, integrityProtectedEncryptedInputStream, null);
}
// Data appears to be OpenPGP message,
// or we handle it as such, since user provided a session-key for decryption
if (openPgpIn.isLikelyOpenPgpMessage() ||
(openPgpIn.isBinaryOpenPgp() && options.getSessionKey() != null)) {
outerDecodingStream = openPgpIn;
objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(outerDecodingStream);
// Parse OpenPGP message
pgpInStream = processPGPPackets(objectFactory, 1);
return new DecryptionStream(pgpInStream,
resultBuilder, integrityProtectedEncryptedInputStream, null);
}
if (openPgpIn.isAsciiArmored()) {
ArmoredInputStream armoredInputStream = ArmoredInputStreamFactory.get(openPgpIn);
if (armoredInputStream.isClearText()) {
resultBuilder.setCleartextSigned();
return parseCleartextSignedMessage(armoredInputStream);
} else {
outerDecodingStream = armoredInputStream;
objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(outerDecodingStream);
// Parse OpenPGP message
pgpInStream = processPGPPackets(objectFactory, 1);
return new DecryptionStream(pgpInStream,
resultBuilder, integrityProtectedEncryptedInputStream,
outerDecodingStream);
}
}
throw new PGPException("Not sure how to handle the input stream.");
}
private DecryptionStream parseCleartextSignedMessage(ArmoredInputStream armorIn)
throws IOException, PGPException {
resultBuilder.setCompressionAlgorithm(CompressionAlgorithm.UNCOMPRESSED)
.setFileEncoding(StreamEncoding.TEXT);
MultiPassStrategy multiPassStrategy = options.getMultiPassStrategy();
PGPSignatureList signatures = ClearsignedMessageUtil.detachSignaturesFromInbandClearsignedMessage(armorIn, multiPassStrategy.getMessageOutputStream());
for (PGPSignature signature : signatures) {
options.addVerificationOfDetachedSignature(signature);
}
initializeDetachedSignatures(options.getDetachedSignatures());
InputStream verifyIn = wrapInVerifySignatureStream(multiPassStrategy.getMessageInputStream(), null);
return new DecryptionStream(verifyIn, resultBuilder, integrityProtectedEncryptedInputStream,
null);
}
private InputStream wrapInVerifySignatureStream(InputStream bufferedIn, @Nullable PGPObjectFactory objectFactory) {
return new SignatureInputStream.VerifySignatures(
bufferedIn, objectFactory, onePassSignatureChecks,
onePassSignaturesWithMissingCert, detachedSignatureChecks, options,
resultBuilder);
}
private InputStream processPGPPackets(@Nonnull PGPObjectFactory objectFactory, int depth)
throws IOException, PGPException {
if (depth >= MAX_PACKET_NESTING_DEPTH) {
throw new PGPException("Maximum depth of nested packages exceeded.");
}
Object nextPgpObject;
try {
while ((nextPgpObject = objectFactory.nextObject()) != null) {
if (nextPgpObject instanceof PGPEncryptedDataList) {
return processPGPEncryptedDataList((PGPEncryptedDataList) nextPgpObject, depth);
}
if (nextPgpObject instanceof PGPCompressedData) {
return processPGPCompressedData((PGPCompressedData) nextPgpObject, depth);
}
if (nextPgpObject instanceof PGPOnePassSignatureList) {
return processOnePassSignatureList(objectFactory, (PGPOnePassSignatureList) nextPgpObject, depth);
}
if (nextPgpObject instanceof PGPLiteralData) {
return processPGPLiteralData(objectFactory, (PGPLiteralData) nextPgpObject, depth);
}
}
} catch (FinalIOException e) {
throw e;
} catch (IOException e) {
if (depth == 1 && e.getMessage().contains("invalid armor")) {
throw e;
}
if (depth == 1 && e.getMessage().contains("unknown object in stream:")) {
throw new MissingLiteralDataException("No Literal Data Packet found.");
} else {
throw new FinalIOException(e);
}
}
throw new MissingLiteralDataException("No Literal Data Packet found");
}
private InputStream processPGPEncryptedDataList(PGPEncryptedDataList pgpEncryptedDataList, int depth)
throws PGPException, IOException {
LOGGER.debug("Depth {}: Encountered PGPEncryptedDataList", depth);
SessionKey sessionKey = options.getSessionKey();
if (sessionKey != null) {
integrityProtectedEncryptedInputStream = decryptWithProvidedSessionKey(pgpEncryptedDataList, sessionKey);
InputStream decodedDataStream = PGPUtil.getDecoderStream(integrityProtectedEncryptedInputStream);
PGPObjectFactory factory = ImplementationFactory.getInstance().getPGPObjectFactory(decodedDataStream);
return processPGPPackets(factory, ++depth);
}
InputStream decryptedDataStream = decryptSessionKey(pgpEncryptedDataList);
InputStream decodedDataStream = PGPUtil.getDecoderStream(decryptedDataStream);
PGPObjectFactory factory = ImplementationFactory.getInstance().getPGPObjectFactory(decodedDataStream);
return processPGPPackets(factory, ++depth);
}
private IntegrityProtectedInputStream decryptWithProvidedSessionKey(
PGPEncryptedDataList pgpEncryptedDataList,
SessionKey sessionKey)
throws PGPException {
SessionKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance()
.getSessionKeyDataDecryptorFactory(sessionKey);
InputStream decryptedDataStream = null;
PGPEncryptedData encryptedData = null;
for (PGPEncryptedData pgpEncryptedData : pgpEncryptedDataList) {
encryptedData = pgpEncryptedData;
if (!options.isIgnoreMDCErrors() && !encryptedData.isIntegrityProtected()) {
throw new MessageNotIntegrityProtectedException();
}
if (encryptedData instanceof PGPPBEEncryptedData) {
PGPPBEEncryptedData pbeEncrypted = (PGPPBEEncryptedData) encryptedData;
decryptedDataStream = pbeEncrypted.getDataStream(decryptorFactory);
break;
} else if (encryptedData instanceof PGPPublicKeyEncryptedData) {
PGPPublicKeyEncryptedData pkEncrypted = (PGPPublicKeyEncryptedData) encryptedData;
decryptedDataStream = pkEncrypted.getDataStream(decryptorFactory);
break;
}
}
if (decryptedDataStream == null) {
throw new PGPException("No valid PGP data encountered.");
}
resultBuilder.setSessionKey(sessionKey);
throwIfAlgorithmIsRejected(sessionKey.getAlgorithm());
integrityProtectedEncryptedInputStream =
new IntegrityProtectedInputStream(decryptedDataStream, encryptedData, options);
return integrityProtectedEncryptedInputStream;
}
private InputStream processPGPCompressedData(PGPCompressedData pgpCompressedData, int depth)
throws PGPException, IOException {
try {
CompressionAlgorithm compressionAlgorithm = CompressionAlgorithm.requireFromId(pgpCompressedData.getAlgorithm());
LOGGER.debug("Depth {}: Encountered PGPCompressedData: {}", depth, compressionAlgorithm);
resultBuilder.setCompressionAlgorithm(compressionAlgorithm);
} catch (NoSuchElementException e) {
throw new PGPException("Unknown compression algorithm encountered.", e);
}
InputStream inflatedDataStream = pgpCompressedData.getDataStream();
InputStream decodedDataStream = PGPUtil.getDecoderStream(inflatedDataStream);
PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(decodedDataStream);
return processPGPPackets(objectFactory, ++depth);
}
private InputStream processOnePassSignatureList(
@Nonnull PGPObjectFactory objectFactory,
PGPOnePassSignatureList onePassSignatures,
int depth)
throws PGPException, IOException {
LOGGER.debug("Depth {}: Encountered PGPOnePassSignatureList of size {}", depth, onePassSignatures.size());
initOnePassSignatures(onePassSignatures);
return processPGPPackets(objectFactory, depth);
}
private InputStream processPGPLiteralData(
@Nonnull PGPObjectFactory objectFactory,
PGPLiteralData pgpLiteralData,
int depth) {
LOGGER.debug("Depth {}: Found PGPLiteralData", depth);
InputStream literalDataInputStream = pgpLiteralData.getInputStream();
resultBuilder.setFileName(pgpLiteralData.getFileName())
.setModificationDate(pgpLiteralData.getModificationTime())
.setFileEncoding(StreamEncoding.requireFromCode(pgpLiteralData.getFormat()));
if (onePassSignatureChecks.isEmpty() && onePassSignaturesWithMissingCert.isEmpty()) {
LOGGER.debug("No OnePassSignatures found -> We are done");
return literalDataInputStream;
}
return new SignatureInputStream.VerifySignatures(literalDataInputStream, objectFactory,
onePassSignatureChecks, onePassSignaturesWithMissingCert, detachedSignatureChecks, options, resultBuilder) {
};
}
private InputStream decryptSessionKey(@Nonnull PGPEncryptedDataList encryptedDataList)
throws PGPException {
Iterator<PGPEncryptedData> encryptedDataIterator = encryptedDataList.getEncryptedDataObjects();
if (!encryptedDataIterator.hasNext()) {
throw new PGPException("Decryption failed - EncryptedDataList has no items");
}
PGPPrivateKey decryptionKey = null;
PGPPublicKeyEncryptedData encryptedSessionKey = null;
List<PGPPBEEncryptedData> passphraseProtected = new ArrayList<>();
List<PGPPublicKeyEncryptedData> publicKeyProtected = new ArrayList<>();
List<Tuple<SubkeyIdentifier, PGPPublicKeyEncryptedData>> postponedDueToMissingPassphrase = new ArrayList<>();
// Sort PKESK and SKESK packets
while (encryptedDataIterator.hasNext()) {
PGPEncryptedData encryptedData = encryptedDataIterator.next();
if (!encryptedData.isIntegrityProtected() && !options.isIgnoreMDCErrors()) {
throw new MessageNotIntegrityProtectedException();
}
// SKESK
if (encryptedData instanceof PGPPBEEncryptedData) {
passphraseProtected.add((PGPPBEEncryptedData) encryptedData);
}
// PKESK
else if (encryptedData instanceof PGPPublicKeyEncryptedData) {
publicKeyProtected.add((PGPPublicKeyEncryptedData) encryptedData);
}
}
// Try decryption with passphrases first
for (PGPPBEEncryptedData pbeEncryptedData : passphraseProtected) {
for (Passphrase passphrase : options.getDecryptionPassphrases()) {
PBEDataDecryptorFactory passphraseDecryptor = ImplementationFactory.getInstance()
.getPBEDataDecryptorFactory(passphrase);
try {
InputStream decryptedDataStream = pbeEncryptedData.getDataStream(passphraseDecryptor);
PGPSessionKey pgpSessionKey = pbeEncryptedData.getSessionKey(passphraseDecryptor);
SessionKey sessionKey = new SessionKey(pgpSessionKey);
resultBuilder.setSessionKey(sessionKey);
throwIfAlgorithmIsRejected(sessionKey.getAlgorithm());
integrityProtectedEncryptedInputStream =
new IntegrityProtectedInputStream(decryptedDataStream, pbeEncryptedData, options);
return integrityProtectedEncryptedInputStream;
} catch (PGPException e) {
LOGGER.debug("Probable passphrase mismatch, skip PBE encrypted data block", e);
}
}
}
// Then try decryption with public key encryption
for (PGPPublicKeyEncryptedData publicKeyEncryptedData : publicKeyProtected) {
PGPPrivateKey privateKey = null;
if (options.getDecryptionKeys().isEmpty()) {
break;
}
long keyId = publicKeyEncryptedData.getKeyID();
// Wildcard KeyID
if (keyId == 0L) {
LOGGER.debug("Hidden recipient detected. Try to decrypt with all available secret keys.");
for (PGPSecretKeyRing secretKeys : options.getDecryptionKeys()) {
if (privateKey != null) {
break;
}
KeyRingInfo info = new KeyRingInfo(secretKeys);
List<PGPPublicKey> encryptionSubkeys = info.getEncryptionSubkeys(EncryptionPurpose.ANY);
for (PGPPublicKey pubkey : encryptionSubkeys) {
PGPSecretKey secretKey = secretKeys.getSecretKey(pubkey.getKeyID());
// Skip missing secret key
if (secretKey == null) {
continue;
}
privateKey = tryPublicKeyDecryption(secretKeys, secretKey, publicKeyEncryptedData,
postponedDueToMissingPassphrase, true);
}
}
}
// Non-wildcard key-id
else {
LOGGER.debug("PGPEncryptedData is encrypted for key {}", Long.toHexString(keyId));
resultBuilder.addRecipientKeyId(keyId);
PGPSecretKeyRing secretKeys = findDecryptionKeyRing(keyId);
if (secretKeys == null) {
LOGGER.debug("Missing certificate of {}. Skip.", Long.toHexString(keyId));
continue;
}
// Make sure that the recipient key is encryption capable and non-expired
KeyRingInfo info = new KeyRingInfo(secretKeys);
List<PGPPublicKey> encryptionSubkeys = info.getEncryptionSubkeys(EncryptionPurpose.ANY);
PGPSecretKey secretKey = null;
for (PGPPublicKey pubkey : encryptionSubkeys) {
if (pubkey.getKeyID() == keyId) {
secretKey = secretKeys.getSecretKey(keyId);
break;
}
}
if (secretKey == null) {
LOGGER.debug("Key " + Long.toHexString(keyId) + " is not valid or not capable for decryption.");
} else {
privateKey = tryPublicKeyDecryption(secretKeys, secretKey, publicKeyEncryptedData,
postponedDueToMissingPassphrase, true);
}
}
if (privateKey == null) {
continue;
}
decryptionKey = privateKey;
encryptedSessionKey = publicKeyEncryptedData;
break;
}
// Try postponed keys with missing passphrases (will cause missing passphrase callbacks to fire)
if (encryptedSessionKey == null) {
if (options.getMissingKeyPassphraseStrategy() == MissingKeyPassphraseStrategy.THROW_EXCEPTION) {
// Non-interactive mode: Throw an exception with all locked decryption keys
Set<SubkeyIdentifier> keyIds = new HashSet<>();
for (Tuple<SubkeyIdentifier, ?> k : postponedDueToMissingPassphrase) {
keyIds.add(k.getA());
}
if (!keyIds.isEmpty()) {
throw new MissingPassphraseException(keyIds);
}
}
else if (options.getMissingKeyPassphraseStrategy() == MissingKeyPassphraseStrategy.INTERACTIVE) {
// Interactive mode: Fire protector callbacks to get passphrases interactively
for (Tuple<SubkeyIdentifier, PGPPublicKeyEncryptedData> missingPassphrases : postponedDueToMissingPassphrase) {
SubkeyIdentifier keyId = missingPassphrases.getA();
PGPPublicKeyEncryptedData publicKeyEncryptedData = missingPassphrases.getB();
PGPSecretKeyRing secretKeys = findDecryptionKeyRing(keyId.getKeyId());
PGPSecretKey secretKey = secretKeys.getSecretKey(keyId.getSubkeyId());
PGPPrivateKey privateKey = tryPublicKeyDecryption(secretKeys, secretKey, publicKeyEncryptedData,
postponedDueToMissingPassphrase, false);
if (privateKey == null) {
continue;
}
decryptionKey = privateKey;
encryptedSessionKey = publicKeyEncryptedData;
break;
}
} else {
throw new IllegalStateException("Invalid PostponedKeysStrategy set in consumer options.");
}
}
return decryptWith(encryptedSessionKey, decryptionKey);
}
/**
* Try decryption of the provided public-key-encrypted-data using the given secret key.
* If the secret key is encrypted and the secret key protector does not have a passphrase available and the boolean
* postponeIfMissingPassphrase is true, data decryption is postponed by pushing a tuple of the encrypted data decryption key
* identifier to the postponed list.
*
* This method only returns a non-null private key, if the private key is able to decrypt the message successfully.
*
* @param secretKeys secret key ring
* @param secretKey secret key
* @param publicKeyEncryptedData encrypted data which is tried to decrypt using the secret key
* @param postponed list of postponed decryptions due to missing secret key passphrases
* @param postponeIfMissingPassphrase flag to specify whether missing secret key passphrases should result in postponed decryption
* @return private key if decryption is successful, null if decryption is unsuccessful or postponed
*
* @throws PGPException in case of an OpenPGP error
*/
private PGPPrivateKey tryPublicKeyDecryption(
PGPSecretKeyRing secretKeys,
PGPSecretKey secretKey,
PGPPublicKeyEncryptedData publicKeyEncryptedData,
List<Tuple<SubkeyIdentifier, PGPPublicKeyEncryptedData>> postponed,
boolean postponeIfMissingPassphrase) throws PGPException {
SecretKeyRingProtector protector = options.getSecretKeyProtector(secretKeys);
if (postponeIfMissingPassphrase && !protector.hasPassphraseFor(secretKey.getKeyID())) {
// Postpone decryption with key with missing passphrase
SubkeyIdentifier identifier = new SubkeyIdentifier(secretKeys, secretKey.getKeyID());
postponed.add(new Tuple<>(identifier, publicKeyEncryptedData));
return null;
}
PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(
secretKey, protector.getDecryptor(secretKey.getKeyID()));
// test if we have the right private key
PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance()
.getPublicKeyDataDecryptorFactory(privateKey);
try {
publicKeyEncryptedData.getSymmetricAlgorithm(decryptorFactory); // will only succeed if we have the right secret key
LOGGER.debug("Found correct decryption key {}.", Long.toHexString(secretKey.getKeyID()));
resultBuilder.setDecryptionKey(new SubkeyIdentifier(secretKeys, privateKey.getKeyID()));
return privateKey;
} catch (PGPException | ClassCastException e) {
return null;
}
}
private InputStream decryptWith(PGPPublicKeyEncryptedData encryptedSessionKey, PGPPrivateKey decryptionKey)
throws PGPException {
if (decryptionKey == null || encryptedSessionKey == null) {
throw new MissingDecryptionMethodException("Decryption failed - No suitable decryption key or passphrase found");
}
PublicKeyDataDecryptorFactory dataDecryptor = ImplementationFactory.getInstance()
.getPublicKeyDataDecryptorFactory(decryptionKey);
PGPSessionKey pgpSessionKey = encryptedSessionKey.getSessionKey(dataDecryptor);
SessionKey sessionKey = new SessionKey(pgpSessionKey);
resultBuilder.setSessionKey(sessionKey);
SymmetricKeyAlgorithm symmetricKeyAlgorithm = sessionKey.getAlgorithm();
if (symmetricKeyAlgorithm == SymmetricKeyAlgorithm.NULL) {
LOGGER.debug("Message is unencrypted");
} else {
LOGGER.debug("Message is encrypted using {}", symmetricKeyAlgorithm);
}
throwIfAlgorithmIsRejected(symmetricKeyAlgorithm);
integrityProtectedEncryptedInputStream = new IntegrityProtectedInputStream(
encryptedSessionKey.getDataStream(dataDecryptor), encryptedSessionKey, options);
return integrityProtectedEncryptedInputStream;
}
private void throwIfAlgorithmIsRejected(SymmetricKeyAlgorithm algorithm)
throws UnacceptableAlgorithmException {
if (!PGPainless.getPolicy().getSymmetricKeyDecryptionAlgorithmPolicy().isAcceptable(algorithm)) {
throw new UnacceptableAlgorithmException("Data is "
+ (algorithm == SymmetricKeyAlgorithm.NULL ?
"unencrypted" :
"encrypted with symmetric algorithm " + algorithm) + " which is not acceptable as per PGPainless' policy.\n" +
"To mark this algorithm as acceptable, use PGPainless.getPolicy().setSymmetricKeyDecryptionAlgorithmPolicy().");
}
}
private void initOnePassSignatures(@Nonnull PGPOnePassSignatureList onePassSignatureList)
throws PGPException {
Iterator<PGPOnePassSignature> iterator = onePassSignatureList.iterator();
if (!iterator.hasNext()) {
throw new PGPException("Verification failed - No OnePassSignatures found");
}
processOnePassSignatures(iterator);
}
private void processOnePassSignatures(Iterator<PGPOnePassSignature> signatures)
throws PGPException {
while (signatures.hasNext()) {
PGPOnePassSignature signature = signatures.next();
processOnePassSignature(signature);
}
}
private void processOnePassSignature(PGPOnePassSignature signature)
throws PGPException {
final long keyId = signature.getKeyID();
LOGGER.debug("Encountered OnePassSignature from {}", Long.toHexString(keyId));
// Find public key
PGPPublicKeyRing verificationKeyRing = findSignatureVerificationKeyRing(keyId);
if (verificationKeyRing == null) {
onePassSignaturesWithMissingCert.put(keyId, new OnePassSignatureCheck(signature, null));
return;
}
PGPPublicKey verificationKey = verificationKeyRing.getPublicKey(keyId);
signature.init(verifierBuilderProvider, verificationKey);
OnePassSignatureCheck onePassSignature = new OnePassSignatureCheck(signature, verificationKeyRing);
onePassSignatureChecks.add(onePassSignature);
}
private PGPSecretKeyRing findDecryptionKeyRing(long keyId) {
for (PGPSecretKeyRing key : options.getDecryptionKeys()) {
if (key.getSecretKey(keyId) != null) {
return key;
}
}
return null;
}
private PGPPublicKeyRing findSignatureVerificationKeyRing(long keyId) {
PGPPublicKeyRing verificationKeyRing = null;
for (PGPPublicKeyRing publicKeyRing : options.getCertificates()) {
PGPPublicKey verificationKey = publicKeyRing.getPublicKey(keyId);
if (verificationKey != null) {
LOGGER.debug("Found public key {} for signature verification", Long.toHexString(keyId));
verificationKeyRing = publicKeyRing;
break;
}
}
if (verificationKeyRing == null && options.getMissingCertificateCallback() != null) {
verificationKeyRing = options.getMissingCertificateCallback().onMissingPublicKeyEncountered(keyId);
}
return verificationKeyRing;
}
}

View file

@ -0,0 +1,49 @@
// 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;
public class IntegrityProtectedInputStream extends InputStream {
private final InputStream inputStream;
private final PGPEncryptedData encryptedData;
private final ConsumerOptions options;
public IntegrityProtectedInputStream(InputStream inputStream, PGPEncryptedData encryptedData, ConsumerOptions options) {
this.inputStream = inputStream;
this.encryptedData = encryptedData;
this.options = options;
}
@Override
public int read() throws IOException {
return inputStream.read();
}
@Override
public int read(@Nonnull byte[] b, int offset, int length) throws IOException {
return inputStream.read(b, offset, length);
}
@Override
public void close() throws IOException {
if (encryptedData.isIntegrityProtected() && !options.isIgnoreMDCErrors()) {
try {
if (!encryptedData.verify()) {
throw new ModificationDetectionException();
}
} catch (PGPException e) {
throw new IOException("Failed to verify integrity protection", e);
}
}
}
}

Some files were not shown because too many files have changed in this diff Show more