pwn 0.5.784 → 0.5.787
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.ruby-version +1 -1
- data/Gemfile +6 -6
- data/README.md +37 -0
- data/bin/pwn +2 -1
- data/bin/pwn_web_cache_deception +35 -7
- data/build_gem.sh +1 -1
- data/documentation/AI-Integration.md +7 -5
- data/documentation/Agent-Tool-Registry.md +2 -2
- data/documentation/Backup-Retention.md +69 -15
- data/documentation/Configuration.md +10 -6
- data/documentation/Contributing.md +1 -1
- data/documentation/Cron.md +2 -2
- data/documentation/Diagrams.md +2 -2
- data/documentation/General-PWN-Usage.md +4 -3
- data/documentation/Harness-Evaluation.md +1 -1
- data/documentation/Home.md +6 -5
- data/documentation/How-PWN-Works.md +6 -5
- data/documentation/Installation.md +12 -9
- data/documentation/Mission-Cancellation.md +134 -0
- data/documentation/Operability.md +18 -0
- data/documentation/Persistence.md +7 -5
- data/documentation/Plugin-Degradation.md +14 -0
- data/documentation/Plugins.md +3 -2
- data/documentation/Policy-Benchmark.md +32 -33
- data/documentation/Runtime-Source-Protection.md +162 -0
- data/documentation/Transparent-Browser.md +11 -0
- data/documentation/Troubleshooting.md +3 -2
- data/documentation/What-is-PWN.md +2 -2
- data/documentation/YubiKey-Vaults.md +185 -0
- data/documentation/YubiKey.md +218 -0
- data/documentation/diagrams/cron-scheduling.svg +130 -130
- data/documentation/diagrams/dot/cron-scheduling.dot +3 -3
- data/documentation/diagrams/dot/persistence-filesystem.dot +5 -3
- data/documentation/diagrams/dot/plugin-ecosystem.dot +2 -2
- data/documentation/diagrams/dot/pwn-ai-feedback-learning-loop.dot +1 -1
- data/documentation/diagrams/dot/reinforcement-learning.dot +2 -2
- data/documentation/diagrams/persistence-filesystem.svg +172 -142
- data/documentation/diagrams/plugin-ecosystem.svg +64 -64
- data/documentation/diagrams/pwn-ai-feedback-learning-loop.svg +1 -1
- data/documentation/diagrams/reinforcement-learning.svg +89 -89
- data/documentation/pwn-ai-Agent.md +10 -4
- data/etc/default_cron/session_cleanup.rb +43 -27
- data/etc/default_skills/pwn/ai/agent/mission_namespace/SKILL.md +21 -0
- data/etc/default_skills/pwn/ai/agent/tools/browser_act/SKILL.md +45 -0
- data/etc/default_skills/pwn/ai/agent/tools/loot_query/SKILL.md +27 -26
- data/etc/default_skills/pwn/ai/agent/tools/recon_run/SKILL.md +25 -15
- data/etc/default_skills/pwn/ai/agent/tools/workspace_tool/SKILL.md +24 -0
- data/etc/default_skills/pwn/ai/server_tools/SKILL.md +25 -0
- data/etc/default_skills/pwn/backups/SKILL.md +54 -0
- data/etc/default_skills/pwn/plugins/recon/SKILL.md +12 -1
- data/etc/default_skills/pwn/plugins/yubikey/SKILL.md +157 -0
- data/etc/default_skills/pwn/plugins/yubikey/fido/SKILL.md +189 -0
- data/etc/default_skills/pwn/plugins/yubikey/hsmauth/SKILL.md +83 -0
- data/etc/default_skills/pwn/plugins/yubikey/management/SKILL.md +80 -0
- data/etc/default_skills/pwn/plugins/yubikey/oath/SKILL.md +115 -0
- data/etc/default_skills/pwn/plugins/yubikey/openpgp/SKILL.md +144 -0
- data/etc/default_skills/pwn/plugins/yubikey/otp/SKILL.md +97 -0
- data/etc/default_skills/pwn/plugins/yubikey/piv/SKILL.md +110 -0
- data/etc/default_skills/pwn/plugins/yubikey/protocol/SKILL.md +21 -0
- data/etc/default_skills/pwn/plugins/yubikey/security_domain/SKILL.md +75 -0
- data/etc/default_skills/pwn/plugins/yubikey/security_domain/scp/SKILL.md +46 -0
- data/etc/default_skills/pwn/plugins/yubikey/transport/SKILL.md +98 -0
- data/etc/default_skills/pwn/redaction/SKILL.md +13 -1
- data/lib/pwn/ai/agent/dispatch.rb +26 -15
- data/lib/pwn/ai/agent/learning.rb +14 -18
- data/lib/pwn/ai/agent/loop.rb +17 -1
- data/lib/pwn/ai/agent/mission_namespace.rb +331 -0
- data/lib/pwn/ai/agent/policy_evaluation.rb +334 -36
- data/lib/pwn/ai/agent/prompt_builder.rb +13 -2
- data/lib/pwn/ai/agent/request_runtime.rb +122 -18
- data/lib/pwn/ai/agent/reward.rb +5 -3
- data/lib/pwn/ai/agent/skill_consolidation.rb +3 -2
- data/lib/pwn/ai/agent/skill_review.rb +9 -5
- data/lib/pwn/ai/agent/swarm.rb +4 -0
- data/lib/pwn/ai/agent/tools/browser_act.rb +32 -0
- data/lib/pwn/ai/agent/tools/loot_query.rb +11 -1
- data/lib/pwn/ai/agent/tools/recon_run.rb +11 -1
- data/lib/pwn/ai/agent/tools/workspace_tool.rb +29 -0
- data/lib/pwn/ai/agent/turn_finalizer.rb +6 -2
- data/lib/pwn/ai/agent.rb +1 -0
- data/lib/pwn/ai/anthropic.rb +8 -33
- data/lib/pwn/ai/cli.rb +34 -5
- data/lib/pwn/ai/gemini.rb +1 -0
- data/lib/pwn/ai/grok.rb +39 -28
- data/lib/pwn/ai/ollama.rb +4 -2
- data/lib/pwn/ai/open_ai.rb +11 -32
- data/lib/pwn/ai/open_web_ui.rb +62 -4
- data/lib/pwn/ai/router.rb +8 -1
- data/lib/pwn/ai/server_tools.rb +231 -0
- data/lib/pwn/ai.rb +1 -0
- data/lib/pwn/backups.rb +349 -0
- data/lib/pwn/config.rb +70 -39
- data/lib/pwn/migrate.rb +85 -280
- data/lib/pwn/module_skills.rb +7 -0
- data/lib/pwn/plugins/ai_sandbox.rb +538 -1
- data/lib/pwn/plugins/authentication_helper.rb +74 -4
- data/lib/pwn/plugins/basic_auth.rb +2 -1
- data/lib/pwn/plugins/google_workspace.rb +4 -21
- data/lib/pwn/plugins/jobs.rb +1 -1
- data/lib/pwn/plugins/log.rb +3 -1
- data/lib/pwn/plugins/preflight_checker.rb +34 -9
- data/lib/pwn/plugins/recon.rb +4 -1
- data/lib/pwn/plugins/repl/ai.rb +293 -26
- data/lib/pwn/plugins/repl/ai_console.rb +268 -88
- data/lib/pwn/plugins/repl/ai_swarm.rb +177 -58
- data/lib/pwn/plugins/repl/mesh.rb +14 -19
- data/lib/pwn/plugins/repl/vault.rb +4 -15
- data/lib/pwn/plugins/repl.rb +9 -216
- data/lib/pwn/plugins/sandbox/driver.rb +4 -0
- data/lib/pwn/plugins/transparent_browser.rb +215 -0
- data/lib/pwn/plugins/vault.rb +412 -135
- data/lib/pwn/plugins/yubikey/fido.rb +1288 -0
- data/lib/pwn/plugins/yubikey/hsmauth.rb +439 -0
- data/lib/pwn/plugins/yubikey/management.rb +686 -0
- data/lib/pwn/plugins/yubikey/oath.rb +766 -0
- data/lib/pwn/plugins/yubikey/openpgp.rb +1063 -0
- data/lib/pwn/plugins/yubikey/otp.rb +897 -0
- data/lib/pwn/plugins/yubikey/piv.rb +1315 -0
- data/lib/pwn/plugins/yubikey/protocol.rb +341 -0
- data/lib/pwn/plugins/yubikey/security_domain/scp.rb +482 -0
- data/lib/pwn/plugins/yubikey/security_domain.rb +464 -0
- data/lib/pwn/plugins/yubikey/transport.rb +1017 -0
- data/lib/pwn/plugins/yubikey.rb +844 -0
- data/lib/pwn/plugins.rb +1 -0
- data/lib/pwn/redaction.rb +69 -12
- data/lib/pwn/session_trace.rb +4 -0
- data/lib/pwn/setup.rb +114 -24
- data/lib/pwn/version.rb +1 -1
- data/lib/pwn/www/app_cobalt_io.rb +16 -5
- data/lib/pwn/www/bug_crowd.rb +16 -5
- data/lib/pwn/www/coinbase_pro.rb +16 -5
- data/lib/pwn/www/github.rb +30 -14
- data/lib/pwn/www/paypal.rb +15 -4
- data/lib/pwn/www/synack.rb +16 -5
- data/lib/pwn/www/twitter.rb +17 -6
- data/lib/pwn.rb +1 -0
- data/spec/documentation/installation_md_spec.rb +1 -1
- data/spec/fixtures/yubikey/apdu_frames.json +162 -0
- data/spec/integration/authentication_helper_oath_spec.rb +117 -0
- data/spec/integration/controller_spill_spec.rb +48 -0
- data/spec/integration/harness_process_spec.rb +97 -0
- data/spec/integration/mission_cancel_spec.rb +79 -0
- data/spec/integration/mission_curses_pty_spec.rb +44 -0
- data/spec/integration/readme_security_spec.rb +47 -0
- data/spec/integration/web_cache_deception_yubikey_spec.rb +159 -0
- data/spec/lib/pwn/ai/agent/dispatch_spec.rb +6 -3
- data/spec/lib/pwn/ai/agent/dispatch_vault_safety_spec.rb +86 -0
- data/spec/lib/pwn/ai/agent/injection_guard_spec.rb +2 -0
- data/spec/lib/pwn/ai/agent/learning_spec.rb +4 -4
- data/spec/lib/pwn/ai/agent/loop_artifacts_spec.rb +5 -0
- data/spec/lib/pwn/ai/agent/loop_spec.rb +104 -29
- data/spec/lib/pwn/ai/agent/mission_namespace_spec.rb +138 -0
- data/spec/lib/pwn/ai/agent/policy_evaluation_spec.rb +10 -23
- data/spec/lib/pwn/ai/agent/request_runtime_spec.rb +48 -0
- data/spec/lib/pwn/ai/agent/shell_search_effect_spec.rb +44 -0
- data/spec/lib/pwn/ai/agent/solve_spec.rb +6 -4
- data/spec/lib/pwn/ai/agent/tools/browser_act_spec.rb +44 -0
- data/spec/lib/pwn/ai/agent/tools/loot_query_yubikey_spec.rb +55 -0
- data/spec/lib/pwn/ai/agent/tools/recon_run_yubikey_spec.rb +43 -0
- data/spec/lib/pwn/ai/agent/tools/workspace_tool_spec.rb +20 -0
- data/spec/lib/pwn/ai/agent/turn_finalizer_spec.rb +7 -0
- data/spec/lib/pwn/ai/anthropic_vault_enrollment_spec.rb +37 -0
- data/spec/lib/pwn/ai/cli_spec.rb +8 -16
- data/spec/lib/pwn/ai/oauth_vault_persistence_spec.rb +195 -0
- data/spec/lib/pwn/ai/open_ai_spec.rb +2 -2
- data/spec/lib/pwn/ai/open_web_ui_workspace_spec.rb +36 -0
- data/spec/lib/pwn/ai/router_spec.rb +1 -1
- data/spec/lib/pwn/ai/server_tools_spec.rb +39 -0
- data/spec/lib/pwn/backups_spec.rb +129 -0
- data/spec/lib/pwn/config_redaction_yubikey_spec.rb +28 -0
- data/spec/lib/pwn/config_yubikey_spec.rb +92 -0
- data/spec/lib/pwn/migrate_spec.rb +29 -81
- data/spec/lib/pwn/migrate_yubikey_spec.rb +128 -0
- data/spec/lib/pwn/module_skills_spec.rb +8 -0
- data/spec/lib/pwn/plugins/ai_sandbox_spec.rb +230 -0
- data/spec/lib/pwn/plugins/authentication_helper_native_yubikey_spec.rb +40 -0
- data/spec/lib/pwn/plugins/authentication_helper_yubikey_spec.rb +90 -0
- data/spec/lib/pwn/plugins/capability_broker/daemon_spec.rb +1 -1
- data/spec/lib/pwn/plugins/google_workspace_spec.rb +17 -35
- data/spec/lib/pwn/plugins/log_yubikey_spec.rb +47 -0
- data/spec/lib/pwn/plugins/preflight_checker_native_yubikey_spec.rb +19 -0
- data/spec/lib/pwn/plugins/preflight_checker_yubikey_spec.rb +55 -0
- data/spec/lib/pwn/plugins/recon_yubikey_spec.rb +43 -0
- data/spec/lib/pwn/plugins/repl/ai_console_spec.rb +110 -75
- data/spec/lib/pwn/plugins/repl/ai_legacy_containment_spec.rb +200 -0
- data/spec/lib/pwn/plugins/repl/ai_swarm_spec.rb +19 -16
- data/spec/lib/pwn/plugins/repl/ai_vault_edit_yubikey_spec.rb +103 -0
- data/spec/lib/pwn/plugins/repl/ai_yubikey_spec.rb +48 -0
- data/spec/lib/pwn/plugins/repl/mesh_vault_persistence_spec.rb +149 -0
- data/spec/lib/pwn/plugins/repl/vault_yaml_serialization_spec.rb +11 -0
- data/spec/lib/pwn/plugins/repl/vault_yubikey_spec.rb +39 -0
- data/spec/lib/pwn/plugins/repl_mesh_layout_spec.rb +2 -0
- data/spec/lib/pwn/plugins/repl_spec.rb +56 -5
- data/spec/lib/pwn/plugins/transparent_browser_jev_spec.rb +46 -0
- data/spec/lib/pwn/plugins/vault_native_yubikey_spec.rb +29 -0
- data/spec/lib/pwn/plugins/vault_publication_safety_spec.rb +258 -0
- data/spec/lib/pwn/plugins/vault_store_safety_spec.rb +241 -0
- data/spec/lib/pwn/plugins/vault_yubikey_safety_spec.rb +168 -0
- data/spec/lib/pwn/plugins/vault_yubikey_spec.rb +128 -0
- data/spec/lib/pwn/plugins/vault_yubikey_vector_spec.rb +45 -0
- data/spec/lib/pwn/plugins/yubikey/fido_spec.rb +854 -0
- data/spec/lib/pwn/plugins/yubikey/hsmauth_spec.rb +235 -0
- data/spec/lib/pwn/plugins/yubikey/management_spec.rb +277 -0
- data/spec/lib/pwn/plugins/yubikey/oath_spec.rb +349 -0
- data/spec/lib/pwn/plugins/yubikey/openpgp_spec.rb +810 -0
- data/spec/lib/pwn/plugins/yubikey/otp_spec.rb +337 -0
- data/spec/lib/pwn/plugins/yubikey/piv_spec.rb +646 -0
- data/spec/lib/pwn/plugins/yubikey/protocol_secure_spec.rb +28 -0
- data/spec/lib/pwn/plugins/yubikey/protocol_spec.rb +149 -0
- data/spec/lib/pwn/plugins/yubikey/protocol_vectors_spec.rb +18 -0
- data/spec/lib/pwn/plugins/yubikey/security_domain/scp_spec.rb +256 -0
- data/spec/lib/pwn/plugins/yubikey/security_domain/support.rb +153 -0
- data/spec/lib/pwn/plugins/yubikey/security_domain_spec.rb +296 -0
- data/spec/lib/pwn/plugins/yubikey/transport_spec.rb +760 -0
- data/spec/lib/pwn/plugins/yubikey_security_spec.rb +260 -0
- data/spec/lib/pwn/plugins/yubikey_spec.rb +209 -0
- data/spec/lib/pwn/plugins/yubikey_terminal_spec.rb +147 -0
- data/spec/lib/pwn/redaction_yubikey_spec.rb +61 -0
- data/spec/lib/pwn/session_cleanup_default_spec.rb +7 -5
- data/spec/lib/pwn/session_cleanup_payload_spec.rb +11 -11
- data/spec/lib/pwn/session_cleanup_recovery_spec.rb +34 -20
- data/spec/lib/pwn/setup_native_yubikey_spec.rb +66 -0
- data/spec/lib/pwn/setup_vault_yubikey_spec.rb +61 -0
- data/spec/lib/pwn/setup_yubikey_spec.rb +40 -0
- data/spec/lib/pwn/www/login_yubikey_spec.rb +273 -0
- data/spec/spec_helper.rb +15 -0
- data/spec/support/harness/cli_faults.rb +13 -9
- data/spec/support/harness/controller_spill.rb +26 -0
- data/spec/support/harness/controller_state.rb +54 -0
- data/spec/support/harness/evaluate.rb +12 -1
- data/spec/support/harness/experiments.rb +1 -1
- data/spec/support/harness/fake_provider.rb +22 -6
- data/spec/support/harness/fault_preload.rb +3 -1
- data/spec/support/harness/mission_console.rb +54 -0
- data/spec/support/harness/mission_curses_pty.rb +41 -0
- data/spec/support/harness/mission_dispatch.rb +30 -0
- data/spec/support/harness/mission_fixture_preload.rb +24 -0
- data/spec/support/harness/mission_job_lifetime.rb +25 -0
- data/spec/support/harness/mission_legacy.rb +25 -0
- data/spec/support/harness/mission_normal_exit.rb +34 -0
- data/spec/support/harness/mission_swarm.rb +23 -0
- data/spec/support/harness/mission_unsupported.rb +29 -0
- data/spec/support/harness/package_acceptance.rb +33 -5
- data/spec/support/harness/session_acceptance.rb +4 -4
- data/spec/support/harness/swarm_acceptance.rb +1 -1
- data/spec/support/harness/swarm_control.rb +44 -0
- data/spec/support/mission_namespace.rb +14 -0
- data/spec/support/mission_pipe_queue.rb +33 -0
- data/third_party/pwn_rdoc.jsonl +605 -19
- metadata +129 -16
- data/scripts/benchmark_policy.rb +0 -376
- data/scripts/build_gate.rb +0 -58
- data/spec/integration/build_gate_spec.rb +0 -73
|
@@ -0,0 +1,897 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'openssl'
|
|
4
|
+
require 'securerandom'
|
|
5
|
+
require 'time'
|
|
6
|
+
require 'pwn/plugins/yubikey/protocol'
|
|
7
|
+
require 'pwn/plugins/yubikey/management'
|
|
8
|
+
|
|
9
|
+
module PWN
|
|
10
|
+
module Plugins
|
|
11
|
+
module YubiKey
|
|
12
|
+
# Native OTP application protocol; no process or interpreter backend.
|
|
13
|
+
# Wire references: Yubico OTP specifications / ykdef.h, and yubikit 5.9.1.
|
|
14
|
+
module OTP
|
|
15
|
+
AID = ['a0000005272001'].pack('H*').freeze
|
|
16
|
+
GRAMMAR = {
|
|
17
|
+
'info' => [0, [], []],
|
|
18
|
+
'swap' => [0, %w[force], []],
|
|
19
|
+
'delete' => [1, %w[force], []],
|
|
20
|
+
'settings' => [1, %w[force delete-access-code enter no-enter use-numeric-keypad serial-usb-visible], %w[pacing]],
|
|
21
|
+
'ndef' => [1, [], %w[prefix ndef-type]],
|
|
22
|
+
'yubiotp' => [1, %w[enter no-enter serial-public-id generate-private-id generate-key upload force], %w[public-id config-output]],
|
|
23
|
+
'static' => [1, %w[generate enter no-enter force], %w[length keyboard-layout]],
|
|
24
|
+
'chalresp' => [1, %w[touch totp generate force], []],
|
|
25
|
+
'calculate' => [2, %w[totp], %w[digits]],
|
|
26
|
+
'hotp' => [1, %w[enter no-enter force], %w[digits counter identifier]]
|
|
27
|
+
}.freeze
|
|
28
|
+
ROUTES = { 'info' => :info, 'swap' => :swap, 'delete' => :delete_slot, 'settings' => :settings, 'ndef' => :configure_ndef, 'yubiotp' => :provision_yubiotp, 'static' => :provision_static, 'chalresp' => :provision_challenge_response, 'calculate' => :calculate, 'hotp' => :provision_hotp }.freeze
|
|
29
|
+
|
|
30
|
+
public_class_method def self.operations(opts = {})
|
|
31
|
+
opts[:format]
|
|
32
|
+
GRAMMAR.keys
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
public_class_method def self.execute(opts = {})
|
|
36
|
+
args = opts[:args] || []
|
|
37
|
+
raise ArgumentError, 'args must be an Array of nonsecret Strings' unless args.is_a?(Array) && args.all?(String)
|
|
38
|
+
return { application: :otp, operations: operations, secrets: :in_memory_only } if args.empty? || args == ['--help']
|
|
39
|
+
|
|
40
|
+
operation = args.first
|
|
41
|
+
grammar = GRAMMAR[operation]
|
|
42
|
+
raise ArgumentError, 'Unknown OTP operation' unless grammar
|
|
43
|
+
|
|
44
|
+
parsed = parse_arguments(args: args.drop(1), flags: grammar[1], values: grammar[2])
|
|
45
|
+
positions = parsed[:positionals]
|
|
46
|
+
limit = grammar[0]
|
|
47
|
+
valid_count = operation == 'calculate' ? positions.length.between?(1, 2) : positions.length == limit
|
|
48
|
+
raise ArgumentError, 'Invalid number of nonsecret OTP arguments; credentials belong in options' unless valid_count
|
|
49
|
+
|
|
50
|
+
selected = opts.merge(parsed[:options])
|
|
51
|
+
if limit.positive?
|
|
52
|
+
raise ArgumentError, 'OTP slot must be 1 or 2' unless %w[1 2].include?(positions.first)
|
|
53
|
+
|
|
54
|
+
selected[:slot] = positions.first.to_i
|
|
55
|
+
end
|
|
56
|
+
selected[:touch] = parsed[:options].fetch(:touch, opts.fetch(:touch, false)) if operation == 'chalresp'
|
|
57
|
+
selected[:details] = true if operation == 'chalresp'
|
|
58
|
+
selected[:challenge_argument] = positions[1] if operation == 'calculate'
|
|
59
|
+
raise ArgumentError, 'Conflicting enter options' if selected[:enter] && selected[:no_enter]
|
|
60
|
+
|
|
61
|
+
public_send(ROUTES.fetch(operation), selected)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
public_class_method def self.info(opts = {})
|
|
65
|
+
selected = { connection: opts[:connection], timeout: opts.fetch(:timeout, 30) }
|
|
66
|
+
state = status(selected)
|
|
67
|
+
if state[:version].first == 4
|
|
68
|
+
metadata = Management.info(selected)
|
|
69
|
+
if metadata[:is_fips]
|
|
70
|
+
state = status(selected) if opts[:connection].descriptor[:protocol] == :apdu
|
|
71
|
+
approved = transfer(selected.merge(command: 0x14, data: ''.b, response_length: 1))
|
|
72
|
+
raise ResponseError, 'Invalid OTP FIPS approval response' unless ["\0".b, "\1".b].include?(approved)
|
|
73
|
+
|
|
74
|
+
state = state.merge(fips_approved: approved == "\1".b)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
state
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
public_class_method def self.delete_slot(opts = {})
|
|
81
|
+
slot = slot_number(slot: opts[:slot])
|
|
82
|
+
consent(opts.merge(confirm: opts[:confirm]))
|
|
83
|
+
access = access_codes(opts)
|
|
84
|
+
before = status(opts)
|
|
85
|
+
require_version(version: before[:version], minimum: [2, 1, 0])
|
|
86
|
+
write_verified(opts.merge(slot: slot, command: slot == 1 ? 1 : 3, data: ("\0".b * 52) + access[:current], before: before, configured: false))
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
public_class_method def self.swap(opts = {})
|
|
90
|
+
consent(opts.merge(confirm: opts[:confirm]))
|
|
91
|
+
before = status(opts)
|
|
92
|
+
require_version(version: before[:version], minimum: [2, 3, 0])
|
|
93
|
+
result = write_verified(opts.merge(command: 6, data: "\0".b * 6, before: before))
|
|
94
|
+
expected = ((before[:flags] & 1) << 1) | ((before[:flags] & 2) >> 1)
|
|
95
|
+
raise ResponseError, 'OTP swap state uncertain: slot readback mismatch; do not replay' unless (result[:status][:flags] & 3) == expected
|
|
96
|
+
|
|
97
|
+
result
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
public_class_method def self.settings(opts = {})
|
|
101
|
+
slot = slot_number(slot: opts[:slot])
|
|
102
|
+
consent(opts.merge(confirm: opts[:confirm]))
|
|
103
|
+
raise ArgumentError, 'Conflicting access-code replacement options' if opts[:delete_access_code] && opts[:new_access_code]
|
|
104
|
+
raise ArgumentError, 'Deleting access protection requires the current access_code' if opts[:delete_access_code] && !opts[:access_code]
|
|
105
|
+
|
|
106
|
+
access = access_codes(opts.merge(new_access_code: opts[:delete_access_code] ? "\0".b * 6 : opts[:new_access_code]))
|
|
107
|
+
fields = keyboard_flags(opts)
|
|
108
|
+
config = pack_config(fields.merge(fixed: "\0".b * 16, uid: "\0".b * 6, key: "\0".b * 16, access_code: access[:new]))
|
|
109
|
+
before = status(opts)
|
|
110
|
+
require_version(version: before[:version], minimum: [2, 3, 0])
|
|
111
|
+
raise CommandError, 'Cannot update an empty OTP slot' unless before[:slots][slot][:configured]
|
|
112
|
+
raise CommandError, 'Firmware 4.3.2 through 4.3.5 cannot update access codes; no write attempted' if (before[:version] <=> [4, 3, 2]) >= 0 && (before[:version] <=> [4, 3, 6]).negative? && access[:new] != access[:current]
|
|
113
|
+
|
|
114
|
+
write_verified(opts.merge(slot: slot, command: slot == 1 ? 4 : 5, data: config + access[:current], before: before, configured: true))
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
public_class_method def self.provision_yubiotp(opts = {})
|
|
118
|
+
slot_number(slot: opts[:slot])
|
|
119
|
+
consent(opts.merge(confirm: opts[:confirm]))
|
|
120
|
+
raise ArgumentError, 'YubiCloud automated upload has ended; no network request is performed' if opts[:upload]
|
|
121
|
+
raise ArgumentError, 'public_id and serial_public_id conflict' if opts[:public_id] && opts[:serial_public_id]
|
|
122
|
+
|
|
123
|
+
fixed = opts[:public_id] ? modhex_decode(value: opts[:public_id]) : nil
|
|
124
|
+
raise ArgumentError, 'public_id must contain at most 16 bytes' if fixed && fixed.bytesize > 16
|
|
125
|
+
raise ArgumentError, 'public_id or serial_public_id is required' unless fixed || opts[:serial_public_id]
|
|
126
|
+
|
|
127
|
+
uid = obtain_secret(opts.merge(name: :private_id, value: opts[:private_id], generate: opts[:generate_private_id], length: 6))
|
|
128
|
+
key = obtain_secret(opts.merge(name: :key, value: opts[:key], generate: opts[:generate_key], length: 16))
|
|
129
|
+
raise ArgumentError, 'private_id must contain six raw bytes and key sixteen raw bytes' unless uid.bytesize == 6 && key.bytesize == 16
|
|
130
|
+
|
|
131
|
+
access_codes(opts)
|
|
132
|
+
keyboard_flags(opts)
|
|
133
|
+
export = open_export(path: opts[:config_output]) if opts[:config_output]
|
|
134
|
+
serial = serial_number(opts) if opts[:serial_public_id] || export
|
|
135
|
+
fixed ||= [0xff, 0].pack('C*') + [serial].pack('N')
|
|
136
|
+
result = program_keyboard(opts.merge(fixed: fixed, uid: uid, key: key, mode_flags: 0))
|
|
137
|
+
if export
|
|
138
|
+
csv = "#{[serial.to_s, fixed.unpack1('H*').tr('0123456789abcdef', MODHEX), uid.unpack1('H*'), key.unpack1('H*'), (opts[:new_access_code] || opts[:access_code])&.unpack1('H*').to_s, Time.now.iso8601, ''].join(',')}\n"
|
|
139
|
+
export.write(csv)
|
|
140
|
+
export.flush
|
|
141
|
+
export.fsync
|
|
142
|
+
result = result.merge(configuration_exported: true)
|
|
143
|
+
end
|
|
144
|
+
result
|
|
145
|
+
rescue IOError, SystemCallError
|
|
146
|
+
raise ResponseError, 'OTP programming/export incomplete; inspect the exact slot and private output file before retrying', cause: nil
|
|
147
|
+
ensure
|
|
148
|
+
export&.close
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
public_class_method def self.provision_static(opts = {})
|
|
152
|
+
slot_number(slot: opts[:slot])
|
|
153
|
+
consent(opts.merge(confirm: opts[:confirm]))
|
|
154
|
+
layout = opts.fetch(:keyboard_layout, 'MODHEX').to_s.upcase
|
|
155
|
+
map = scan_map(layout: layout)
|
|
156
|
+
length = strict_integer(value: opts.fetch(:length, 38), minimum: 1, maximum: 38)
|
|
157
|
+
if opts[:generate]
|
|
158
|
+
raise ArgumentError, 'generate conflicts with password' if opts[:password]
|
|
159
|
+
raise ArgumentError, 'Generated credentials require an explicit secret_sink callable' unless opts[:secret_sink].respond_to?(:call)
|
|
160
|
+
|
|
161
|
+
alphabet = map.keys.grep_v(/\s/)
|
|
162
|
+
password = Array.new(length) { alphabet[SecureRandom.random_number(alphabet.length)] }.join
|
|
163
|
+
opts[:secret_sink].call(:password, password.dup)
|
|
164
|
+
else
|
|
165
|
+
password = secret_value(opts.merge(name: :password, value: opts[:password])).force_encoding(Encoding::UTF_8)
|
|
166
|
+
end
|
|
167
|
+
raise ArgumentError, 'password must contain 1 to 38 supported characters' unless password.valid_encoding? && password.length.between?(1, 38)
|
|
168
|
+
|
|
169
|
+
codes = password.each_char.map do |char|
|
|
170
|
+
raise ArgumentError, 'Password contains a character unavailable in the selected keyboard layout' unless map.key?(char)
|
|
171
|
+
|
|
172
|
+
map[char]
|
|
173
|
+
end.pack('C*').ljust(38, "\0")
|
|
174
|
+
program_keyboard(opts.merge(fixed: codes.byteslice(0, 16), uid: codes.byteslice(16, 6), key: codes.byteslice(22, 16), mode_flags: 2))
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
public_class_method def self.provision_hotp(opts = {})
|
|
178
|
+
slot_number(slot: opts[:slot])
|
|
179
|
+
consent(opts.merge(confirm: opts[:confirm]))
|
|
180
|
+
digits = strict_integer(value: opts.fetch(:digits, 6), minimum: 6, maximum: 8)
|
|
181
|
+
raise ArgumentError, 'HOTP digits must be 6 or 8' unless [6, 8].include?(digits)
|
|
182
|
+
|
|
183
|
+
counter = strict_integer(value: opts.fetch(:counter, 0), minimum: 0, maximum: 0xffff0)
|
|
184
|
+
raise ArgumentError, 'HOTP counter must be divisible by 16' unless (counter % 16).zero?
|
|
185
|
+
|
|
186
|
+
identifier = hotp_identifier(opts.merge(identifier: opts[:identifier]))
|
|
187
|
+
key = secret_value(opts.merge(name: :key, value: opts[:key]))
|
|
188
|
+
key = OpenSSL::Digest::SHA1.digest(key) if key.bytesize > 64
|
|
189
|
+
raise ArgumentError, 'HOTP key must contain 1 to 20 bytes, or more than 64 bytes' unless key.bytesize.between?(1, 20)
|
|
190
|
+
|
|
191
|
+
uid = key.byteslice(16..).to_s.b.ljust(4, "\0") + [counter >> 4].pack('n')
|
|
192
|
+
program_keyboard(opts.merge(fixed: identifier[:fixed], uid: uid, key: key.byteslice(0, 16).ljust(16, "\0"), mode_flags: identifier[:flags] | (digits == 8 ? 2 : 0), ticket_flags: 0x40))
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
public_class_method def self.configure_ndef(opts = {})
|
|
196
|
+
slot = slot_number(slot: opts[:slot])
|
|
197
|
+
consent(opts.merge(confirm: opts[:confirm]))
|
|
198
|
+
type = opts.fetch(:ndef_type, 'URI').to_s.upcase
|
|
199
|
+
raise ArgumentError, 'ndef_type must be URI or TEXT' unless %w[URI TEXT].include?(type)
|
|
200
|
+
|
|
201
|
+
prefix = opts.fetch(:prefix, type == 'URI' ? 'https://my.yubico.com/yk/#' : '')
|
|
202
|
+
raise ArgumentError, 'NDEF prefix must be a valid UTF8 String' unless prefix.is_a?(String) && prefix.encoding == Encoding::UTF_8 && prefix.valid_encoding?
|
|
203
|
+
|
|
204
|
+
payload = if type == 'TEXT'
|
|
205
|
+
"\2en".b + prefix.b
|
|
206
|
+
else
|
|
207
|
+
index = NDEF_PREFIXES.index { |candidate| prefix.start_with?(candidate) }
|
|
208
|
+
index ? [index + 1].pack('C') + prefix.delete_prefix(NDEF_PREFIXES[index]).b : "\0".b + prefix.b
|
|
209
|
+
end
|
|
210
|
+
raise ArgumentError, 'NDEF payload must not exceed 54 bytes' if payload.bytesize > 54
|
|
211
|
+
|
|
212
|
+
access = access_codes(opts)
|
|
213
|
+
validate_nfc(connection: opts[:connection], timeout: opts.fetch(:timeout, 30))
|
|
214
|
+
before = status(opts)
|
|
215
|
+
require_version(version: before[:version], minimum: [3, 0, 0])
|
|
216
|
+
raise CommandError, 'NDEF requires a configured slot' unless before[:slots][slot][:configured]
|
|
217
|
+
raise CommandError, 'NDEF is unavailable on this device' if opts[:connection].descriptor[:nfc] == false
|
|
218
|
+
|
|
219
|
+
data = [payload.bytesize, type == 'URI' ? 0x55 : 0x54].pack('C*') + payload.ljust(54, "\0") + access[:current]
|
|
220
|
+
write_verified(opts.merge(slot: slot, command: slot == 1 ? 8 : 9, data: data, before: before, configured: true))
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
public_class_method def self.calculate(opts = {})
|
|
224
|
+
argument = opts[:challenge_argument]
|
|
225
|
+
if opts[:totp]
|
|
226
|
+
digits = strict_integer(value: opts.fetch(:digits, 6), minimum: 6, maximum: 8)
|
|
227
|
+
raise ArgumentError, 'TOTP digits must be 6 or 8' unless [6, 8].include?(digits)
|
|
228
|
+
|
|
229
|
+
timestamp = strict_integer(value: argument || opts.fetch(:timestamp) { Time.now.to_i }, minimum: 0, maximum: 0xffffffffffffffff)
|
|
230
|
+
challenge = [timestamp / 30].pack('Q>')
|
|
231
|
+
else
|
|
232
|
+
challenge = opts[:challenge]
|
|
233
|
+
if argument
|
|
234
|
+
raise ArgumentError, 'Challenge argument must be an even-length hexadecimal String' unless argument.match?(/\A(?:[0-9a-fA-F]{2}){1,64}\z/)
|
|
235
|
+
|
|
236
|
+
challenge = [argument].pack('H*')
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
response = challenge_response(opts.merge(challenge: challenge))
|
|
240
|
+
return response unless opts[:totp]
|
|
241
|
+
|
|
242
|
+
offset = response.getbyte(19) & 15
|
|
243
|
+
value = response.byteslice(offset, 4).unpack1('N') & 0x7fffffff
|
|
244
|
+
(value % (10**digits)).to_s.rjust(digits, '0')
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
NDEF_PREFIXES = %w[http://www. https://www. http:// https:// tel: mailto: ftp://anonymous:anonymous@ ftp://ftp. ftps:// sftp:// smb:// nfs:// ftp:// dav:// news: telnet:// imap: rtsp:// urn: pop: sip: sips: tftp: btspp:// btl2cap:// btgoep:// tcpobex:// irdaobex:// file:// urn:epc:id: urn:epc:tag: urn:epc:pat: urn:epc:raw: urn:epc: urn:nfc:].freeze
|
|
248
|
+
# Keyboard layout data adapted from yubikey-manager 5.9.1.
|
|
249
|
+
# Copyright (c) 2018 Yubico AB. All rights reserved.
|
|
250
|
+
# Redistribution and use in source and binary forms, with or without
|
|
251
|
+
# modification, are permitted provided that the following conditions are met:
|
|
252
|
+
# 1. Redistributions of source code must retain the above copyright notice,
|
|
253
|
+
# this list of conditions and the following disclaimer.
|
|
254
|
+
# 2. Redistributions in binary form must reproduce the above copyright
|
|
255
|
+
# notice, this list of conditions and the following disclaimer in the
|
|
256
|
+
# documentation and/or other materials provided with the distribution.
|
|
257
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
258
|
+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
259
|
+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
260
|
+
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
|
261
|
+
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
262
|
+
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
263
|
+
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
264
|
+
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
265
|
+
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
266
|
+
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
267
|
+
# POSSIBILITY OF SUCH DAMAGE.
|
|
268
|
+
|
|
269
|
+
KEYBOARD_LAYOUTS = {
|
|
270
|
+
'US' => {
|
|
271
|
+
97 => 0x04, 98 => 0x05, 99 => 0x06, 100 => 0x07, 101 => 0x08, 102 => 0x09, 103 => 0x0a, 104 => 0x0b,
|
|
272
|
+
105 => 0x0c, 106 => 0x0d, 107 => 0x0e, 108 => 0x0f, 109 => 0x10, 110 => 0x11, 111 => 0x12, 112 => 0x13,
|
|
273
|
+
113 => 0x14, 114 => 0x15, 115 => 0x16, 116 => 0x17, 117 => 0x18, 118 => 0x19, 119 => 0x1a, 120 => 0x1b,
|
|
274
|
+
121 => 0x1c, 122 => 0x1d, 65 => 0x84, 66 => 0x85, 67 => 0x86, 68 => 0x87, 69 => 0x88, 70 => 0x89,
|
|
275
|
+
71 => 0x8a, 72 => 0x8b, 73 => 0x8c, 74 => 0x8d, 75 => 0x8e, 76 => 0x8f, 77 => 0x90, 78 => 0x91,
|
|
276
|
+
79 => 0x92, 80 => 0x93, 81 => 0x94, 82 => 0x95, 83 => 0x96, 84 => 0x97, 85 => 0x98, 86 => 0x99,
|
|
277
|
+
87 => 0x9a, 88 => 0x9b, 89 => 0x9c, 90 => 0x9d, 48 => 0x27, 49 => 0x1e, 50 => 0x1f, 51 => 0x20,
|
|
278
|
+
52 => 0x21, 53 => 0x22, 54 => 0x23, 55 => 0x24, 56 => 0x25, 57 => 0x26, 9 => 0x2b, 10 => 0x28,
|
|
279
|
+
33 => 0x9e, 34 => 0xb4, 35 => 0xa0, 36 => 0xa1, 37 => 0xa2, 38 => 0xa4, 39 => 0x34, 96 => 0x35,
|
|
280
|
+
40 => 0xa6, 41 => 0xa7, 42 => 0xa5, 43 => 0xae, 44 => 0x36, 45 => 0x2d, 46 => 0x37, 47 => 0x38,
|
|
281
|
+
58 => 0xb3, 59 => 0x33, 60 => 0xb6, 61 => 0x2e, 62 => 0xb7, 63 => 0xb8, 64 => 0x9f, 91 => 0x2f,
|
|
282
|
+
92 => 0x32, 93 => 0x30, 94 => 0xa3, 95 => 0xad, 123 => 0xaf, 125 => 0xb0, 124 => 0xb2, 126 => 0xb5,
|
|
283
|
+
32 => 0x2c
|
|
284
|
+
},
|
|
285
|
+
'MODHEX' => {
|
|
286
|
+
98 => 0x05, 99 => 0x06, 100 => 0x07, 101 => 0x08, 102 => 0x09, 103 => 0x0a, 104 => 0x0b, 105 => 0x0c,
|
|
287
|
+
106 => 0x0d, 107 => 0x0e, 108 => 0x0f, 110 => 0x11, 114 => 0x15, 116 => 0x17, 117 => 0x18, 118 => 0x19,
|
|
288
|
+
66 => 0x85, 67 => 0x86, 68 => 0x87, 69 => 0x88, 70 => 0x89, 71 => 0x8a, 72 => 0x8b, 73 => 0x8c,
|
|
289
|
+
74 => 0x8d, 75 => 0x8e, 76 => 0x8f, 78 => 0x91, 82 => 0x95, 84 => 0x97, 85 => 0x98, 86 => 0x99
|
|
290
|
+
},
|
|
291
|
+
'UK' => {
|
|
292
|
+
97 => 0x04, 98 => 0x05, 99 => 0x06, 100 => 0x07, 101 => 0x08, 102 => 0x09, 103 => 0x0a, 104 => 0x0b,
|
|
293
|
+
105 => 0x0c, 106 => 0x0d, 107 => 0x0e, 108 => 0x0f, 109 => 0x10, 110 => 0x11, 111 => 0x12, 112 => 0x13,
|
|
294
|
+
113 => 0x14, 114 => 0x15, 115 => 0x16, 116 => 0x17, 117 => 0x18, 118 => 0x19, 119 => 0x1a, 120 => 0x1b,
|
|
295
|
+
121 => 0x1c, 122 => 0x1d, 65 => 0x84, 66 => 0x85, 67 => 0x86, 68 => 0x87, 69 => 0x88, 70 => 0x89,
|
|
296
|
+
71 => 0x8a, 72 => 0x8b, 73 => 0x8c, 74 => 0x8d, 75 => 0x8e, 76 => 0x8f, 77 => 0x90, 78 => 0x91,
|
|
297
|
+
79 => 0x92, 80 => 0x93, 81 => 0x94, 82 => 0x95, 83 => 0x96, 84 => 0x97, 85 => 0x98, 86 => 0x99,
|
|
298
|
+
87 => 0x9a, 88 => 0x9b, 89 => 0x9c, 90 => 0x9d, 48 => 0x27, 49 => 0x1e, 50 => 0x1f, 51 => 0x20,
|
|
299
|
+
52 => 0x21, 53 => 0x22, 54 => 0x23, 55 => 0x24, 56 => 0x25, 57 => 0x26, 9 => 0x2b, 10 => 0x28,
|
|
300
|
+
33 => 0x9e, 64 => 0xb4, 163 => 0xa0, 36 => 0xa1, 37 => 0xa2, 38 => 0xa4, 39 => 0x34, 96 => 0x35,
|
|
301
|
+
40 => 0xa6, 41 => 0xa7, 42 => 0xa5, 43 => 0xae, 44 => 0x36, 45 => 0x2d, 46 => 0x37, 47 => 0x38,
|
|
302
|
+
58 => 0xb3, 59 => 0x33, 60 => 0xb6, 61 => 0x2e, 62 => 0xb7, 63 => 0xb8, 34 => 0x9f, 91 => 0x2f,
|
|
303
|
+
35 => 0x32, 93 => 0x30, 94 => 0xa3, 95 => 0xad, 123 => 0xaf, 125 => 0xb0, 126 => 0xb2, 172 => 0xb5,
|
|
304
|
+
32 => 0x2c
|
|
305
|
+
},
|
|
306
|
+
'DE' => {
|
|
307
|
+
97 => 0x04, 98 => 0x05, 99 => 0x06, 100 => 0x07, 101 => 0x08, 102 => 0x09, 103 => 0x0a, 104 => 0x0b,
|
|
308
|
+
105 => 0x0c, 106 => 0x0d, 107 => 0x0e, 108 => 0x0f, 109 => 0x10, 110 => 0x11, 111 => 0x12, 112 => 0x13,
|
|
309
|
+
113 => 0x14, 114 => 0x15, 115 => 0x16, 116 => 0x17, 117 => 0x18, 118 => 0x19, 119 => 0x1a, 120 => 0x1b,
|
|
310
|
+
121 => 0x1d, 122 => 0x1c, 65 => 0x84, 66 => 0x85, 67 => 0x86, 68 => 0x87, 69 => 0x88, 70 => 0x89,
|
|
311
|
+
71 => 0x8a, 72 => 0x8b, 73 => 0x8c, 74 => 0x8d, 75 => 0x8e, 76 => 0x8f, 77 => 0x90, 78 => 0x91,
|
|
312
|
+
79 => 0x92, 80 => 0x93, 81 => 0x94, 82 => 0x95, 83 => 0x96, 84 => 0x97, 85 => 0x98, 86 => 0x99,
|
|
313
|
+
87 => 0x9a, 88 => 0x9b, 89 => 0x9d, 90 => 0x9c, 48 => 0x27, 49 => 0x1e, 50 => 0x1f, 51 => 0x20,
|
|
314
|
+
52 => 0x21, 53 => 0x22, 54 => 0x23, 55 => 0x24, 56 => 0x25, 57 => 0x26, 9 => 0x2b, 10 => 0x28,
|
|
315
|
+
33 => 0x9e, 34 => 0x9f, 35 => 0x32, 36 => 0xa1, 37 => 0xa2, 38 => 0xa3, 39 => 0xb2, 40 => 0xa5,
|
|
316
|
+
41 => 0xa6, 42 => 0xb0, 43 => 0x30, 44 => 0x36, 45 => 0x38, 46 => 0x37, 47 => 0xa4, 58 => 0xb7,
|
|
317
|
+
59 => 0xb6, 60 => 0x64, 61 => 0xa7, 62 => 0xe4, 63 => 0xad, 94 => 0x35, 95 => 0xb8, 32 => 0x2c,
|
|
318
|
+
96 => 0xad, 167 => 0xa0, 180 => 0x2e, 196 => 0xb4, 214 => 0xb3, 220 => 0xaf, 223 => 0x2d, 228 => 0x34,
|
|
319
|
+
246 => 0x33, 252 => 0x2f
|
|
320
|
+
},
|
|
321
|
+
'FR' => {
|
|
322
|
+
97 => 0x14, 98 => 0x05, 99 => 0x06, 100 => 0x07, 101 => 0x08, 102 => 0x09, 103 => 0x0a, 104 => 0x0b,
|
|
323
|
+
105 => 0x0c, 106 => 0x0d, 107 => 0x0e, 108 => 0x0f, 109 => 0x33, 110 => 0x11, 111 => 0x12, 112 => 0x13,
|
|
324
|
+
113 => 0x04, 114 => 0x15, 115 => 0x16, 116 => 0x17, 117 => 0x18, 118 => 0x19, 119 => 0x1d, 120 => 0x1b,
|
|
325
|
+
121 => 0x1c, 122 => 0x1a, 65 => 0x94, 66 => 0x85, 67 => 0x86, 68 => 0x87, 69 => 0x88, 70 => 0x89,
|
|
326
|
+
71 => 0x8a, 72 => 0x8b, 73 => 0x8c, 74 => 0x8d, 75 => 0x8e, 76 => 0x8f, 77 => 0xb3, 78 => 0x91,
|
|
327
|
+
79 => 0x92, 80 => 0x93, 81 => 0x84, 82 => 0x95, 83 => 0x96, 84 => 0x97, 85 => 0x98, 86 => 0x99,
|
|
328
|
+
87 => 0x9d, 88 => 0x9b, 89 => 0x9c, 90 => 0x9a, 48 => 0xa7, 49 => 0x9e, 50 => 0x9f, 51 => 0xa0,
|
|
329
|
+
52 => 0xa1, 53 => 0xa2, 54 => 0xa3, 55 => 0xa4, 56 => 0xa5, 57 => 0xa6, 9 => 0x2b, 10 => 0x28,
|
|
330
|
+
32 => 0x2c, 33 => 0x38, 34 => 0x20, 36 => 0x30, 37 => 0xb4, 38 => 0x1e, 39 => 0x21, 40 => 0x22,
|
|
331
|
+
41 => 0x2d, 42 => 0x31, 43 => 0xae, 44 => 0x10, 45 => 0x23, 46 => 0xb6, 47 => 0xb7, 58 => 0x37,
|
|
332
|
+
59 => 0x36, 60 => 0x64, 61 => 0x2e, 95 => 0x25, 127 => 0x2a, 163 => 0xb0, 167 => 0xb8, 176 => 0xad,
|
|
333
|
+
178 => 0x35, 181 => 0xb1, 224 => 0x27, 231 => 0x26, 232 => 0x24, 233 => 0x1f, 249 => 0x34
|
|
334
|
+
},
|
|
335
|
+
'IT' => {
|
|
336
|
+
9 => 0x2b, 10 => 0x28, 32 => 0x2c, 33 => 0x9e, 34 => 0x9f, 35 => 0x32, 36 => 0xa1, 37 => 0xa2,
|
|
337
|
+
38 => 0xa3, 39 => 0x2d, 40 => 0xa5, 41 => 0xa6, 42 => 0x55, 43 => 0x30, 44 => 0x36, 45 => 0x38,
|
|
338
|
+
46 => 0x63, 47 => 0xa4, 48 => 0x27, 49 => 0x1e, 50 => 0x1f, 51 => 0x20, 52 => 0x21, 53 => 0x22,
|
|
339
|
+
54 => 0x23, 55 => 0x24, 56 => 0x25, 57 => 0x26, 58 => 0xb7, 59 => 0xb6, 60 => 0x64, 61 => 0xa7,
|
|
340
|
+
62 => 0xe4, 63 => 0xad, 64 => 0x24, 65 => 0x84, 66 => 0x85, 67 => 0x86, 68 => 0x87, 69 => 0x88,
|
|
341
|
+
70 => 0x89, 71 => 0x8a, 72 => 0x8b, 73 => 0x8c, 74 => 0x8d, 75 => 0x8e, 76 => 0x8f, 77 => 0x90,
|
|
342
|
+
78 => 0x91, 79 => 0x92, 80 => 0x93, 81 => 0x94, 82 => 0x95, 83 => 0x96, 84 => 0x97, 85 => 0x98,
|
|
343
|
+
86 => 0x99, 87 => 0x9a, 88 => 0x9b, 89 => 0x9c, 90 => 0x9d, 92 => 0x35, 94 => 0xae, 95 => 0xb8,
|
|
344
|
+
96 => 0xad, 97 => 0x04, 98 => 0x05, 99 => 0x06, 100 => 0x07, 101 => 0x08, 102 => 0x09, 103 => 0x0a,
|
|
345
|
+
104 => 0x0b, 105 => 0x0c, 106 => 0x0d, 107 => 0x0e, 108 => 0x0f, 109 => 0x10, 110 => 0x11, 111 => 0x12,
|
|
346
|
+
112 => 0x13, 113 => 0x14, 114 => 0x15, 115 => 0x16, 116 => 0x17, 117 => 0x18, 118 => 0x19, 119 => 0x1a,
|
|
347
|
+
120 => 0x1b, 121 => 0x1c, 122 => 0x1d, 124 => 0xb5, 163 => 0xa0, 167 => 0xb2, 176 => 0xb4, 231 => 0xb3,
|
|
348
|
+
232 => 0x2f, 233 => 0xaf, 224 => 0x34, 236 => 0x2e, 242 => 0x33, 249 => 0x31
|
|
349
|
+
},
|
|
350
|
+
'BEPO' => {
|
|
351
|
+
9 => 0xab, 10 => 0xa8, 32 => 0x2c, 33 => 0x9c, 34 => 0x1e, 35 => 0xb5, 36 => 0x35, 37 => 0x2e,
|
|
352
|
+
39 => 0x11, 40 => 0x21, 41 => 0x22, 42 => 0x27, 43 => 0x24, 44 => 0x0a, 45 => 0x25, 46 => 0x19,
|
|
353
|
+
47 => 0x26, 48 => 0xa7, 49 => 0x9e, 50 => 0x9f, 51 => 0xa0, 52 => 0xa1, 53 => 0xa2, 54 => 0xa3,
|
|
354
|
+
55 => 0xa4, 56 => 0xa5, 57 => 0xa6, 58 => 0x99, 59 => 0x8a, 61 => 0x2d, 63 => 0x91, 64 => 0x23,
|
|
355
|
+
65 => 0x84, 66 => 0x94, 67 => 0x8b, 68 => 0x8c, 69 => 0x89, 70 => 0xb8, 71 => 0xb6, 72 => 0xb7,
|
|
356
|
+
73 => 0x87, 74 => 0x93, 75 => 0x85, 76 => 0x92, 77 => 0xb4, 78 => 0xb3, 79 => 0x95, 80 => 0x88,
|
|
357
|
+
81 => 0x90, 82 => 0x8f, 83 => 0x8e, 84 => 0x8d, 85 => 0x96, 86 => 0x98, 87 => 0xb0, 88 => 0x86,
|
|
358
|
+
89 => 0x9b, 90 => 0xaf, 96 => 0xae, 97 => 0x04, 98 => 0x14, 99 => 0x0b, 100 => 0x0c, 101 => 0x09,
|
|
359
|
+
102 => 0x38, 103 => 0x36, 104 => 0x37, 105 => 0x07, 106 => 0x13, 107 => 0x05, 108 => 0x12, 109 => 0x34,
|
|
360
|
+
110 => 0x33, 111 => 0x15, 112 => 0x08, 113 => 0x10, 114 => 0x0f, 115 => 0x0e, 116 => 0x0d, 117 => 0x16,
|
|
361
|
+
118 => 0x18, 119 => 0x30, 120 => 0x06, 121 => 0x1b, 122 => 0x2f, 160 => 0xac, 171 => 0x1f, 176 => 0xad,
|
|
362
|
+
187 => 0x20, 192 => 0x9d, 199 => 0xb1, 200 => 0x97, 201 => 0x9a, 202 => 0xe4, 224 => 0x1d, 231 => 0x31,
|
|
363
|
+
232 => 0x17, 233 => 0x1a, 234 => 0x64
|
|
364
|
+
},
|
|
365
|
+
'NORMAN' => {
|
|
366
|
+
97 => 0x04, 98 => 0x05, 99 => 0x06, 100 => 0x08, 101 => 0x07, 102 => 0x15, 103 => 0x0a, 104 => 0x33,
|
|
367
|
+
105 => 0x0e, 106 => 0x1c, 107 => 0x17, 108 => 0x12, 109 => 0x10, 110 => 0x0d, 111 => 0x0f, 112 => 0x11,
|
|
368
|
+
113 => 0x14, 114 => 0x0c, 115 => 0x16, 116 => 0x09, 117 => 0x18, 118 => 0x19, 119 => 0x1a, 120 => 0x1b,
|
|
369
|
+
121 => 0x0b, 122 => 0x1d, 65 => 0x84, 66 => 0x85, 67 => 0x86, 68 => 0x88, 69 => 0x87, 70 => 0x95,
|
|
370
|
+
71 => 0x8a, 72 => 0xb3, 73 => 0x8e, 74 => 0x9c, 75 => 0x97, 76 => 0x92, 77 => 0x90, 78 => 0x8d,
|
|
371
|
+
79 => 0x8f, 80 => 0x91, 81 => 0x94, 82 => 0x8c, 83 => 0x96, 84 => 0x89, 85 => 0x98, 86 => 0x99,
|
|
372
|
+
87 => 0x9a, 88 => 0x9b, 89 => 0x8b, 90 => 0x9d, 48 => 0x27, 49 => 0x1e, 50 => 0x1f, 51 => 0x20,
|
|
373
|
+
52 => 0x21, 53 => 0x22, 54 => 0x23, 55 => 0x24, 56 => 0x25, 57 => 0x26, 9 => 0x2b, 10 => 0x28,
|
|
374
|
+
33 => 0x9e, 34 => 0xb4, 35 => 0xa0, 36 => 0xa1, 37 => 0xa2, 38 => 0xa4, 39 => 0x34, 96 => 0x35,
|
|
375
|
+
40 => 0xa6, 41 => 0xa7, 42 => 0xa5, 43 => 0xae, 44 => 0x36, 45 => 0x2d, 46 => 0x37, 47 => 0x38,
|
|
376
|
+
58 => 0xb3, 59 => 0x13, 60 => 0xb6, 61 => 0x2e, 62 => 0xb7, 63 => 0xb8, 64 => 0x9f, 91 => 0x2f,
|
|
377
|
+
92 => 0x32, 93 => 0x30, 94 => 0xa3, 95 => 0xad, 123 => 0xaf, 125 => 0xb0, 124 => 0xb2, 126 => 0xb5,
|
|
378
|
+
32 => 0x2c
|
|
379
|
+
}
|
|
380
|
+
}.transform_values { |mapping| mapping.transform_keys { |codepoint| codepoint.chr(Encoding::UTF_8) }.freeze }.freeze
|
|
381
|
+
|
|
382
|
+
MODHEX = 'cbdefghijklnrtuv'
|
|
383
|
+
|
|
384
|
+
private_class_method def self.program_keyboard(opts = {})
|
|
385
|
+
slot = opts[:slot]
|
|
386
|
+
access = access_codes(opts)
|
|
387
|
+
fields = keyboard_flags(opts)
|
|
388
|
+
fields[:cfg] |= opts[:mode_flags]
|
|
389
|
+
fields[:tkt] |= opts.fetch(:ticket_flags, 0)
|
|
390
|
+
config = pack_config(fields.merge(fixed: opts[:fixed], uid: opts[:uid], key: opts[:key], access_code: access[:new]))
|
|
391
|
+
before = status(opts)
|
|
392
|
+
require_version(version: before[:version], minimum: [2, 2, 0])
|
|
393
|
+
write_verified(opts.merge(command: slot == 1 ? 1 : 3, data: config + access[:current], before: before, configured: true))
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
private_class_method def self.hotp_identifier(opts = {})
|
|
397
|
+
value = opts[:identifier]
|
|
398
|
+
return { fixed: ''.b, flags: 0 } if value.nil?
|
|
399
|
+
raise ArgumentError, 'identifier must be a String' unless value.is_a?(String)
|
|
400
|
+
|
|
401
|
+
value = 'ubhe' if value == '-'
|
|
402
|
+
value += format('%08d', serial_number(opts)) if value.length == 4
|
|
403
|
+
value = "ubhe#{value}" if value.length == 8
|
|
404
|
+
raise ArgumentError, 'identifier must encode 12 modhex/BCD characters' unless value.length == 12
|
|
405
|
+
|
|
406
|
+
parts = [value[0, 2], value[2, 2], value[4, 8]]
|
|
407
|
+
modes = parts.map { |part| part.each_char.all? { |char| MODHEX.include?(char) } }
|
|
408
|
+
raise ArgumentError, 'Identifier modhex components must form a leading prefix' if (!modes[0] && modes[1]) || (modes[2] && !modes.take(2).all?)
|
|
409
|
+
|
|
410
|
+
fixed = parts.each_with_index.map do |part, index|
|
|
411
|
+
next modhex_decode(value: part) if modes[index]
|
|
412
|
+
raise ArgumentError, 'Identifier component is neither modhex nor BCD' unless part.match?(/\A[0-9]+\z/)
|
|
413
|
+
|
|
414
|
+
[part].pack('H*')
|
|
415
|
+
end.join.b
|
|
416
|
+
{ fixed: fixed, flags: if modes[2]
|
|
417
|
+
0x50
|
|
418
|
+
elsif modes[1]
|
|
419
|
+
0x40
|
|
420
|
+
else
|
|
421
|
+
modes[0] ? 0x10 : 0
|
|
422
|
+
end }
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
private_class_method def self.modhex_decode(opts = {})
|
|
426
|
+
value = opts[:value]
|
|
427
|
+
raise ArgumentError, 'public_id must be even-length lowercase modhex' unless value.is_a?(String) && value.match?(/\A(?:[cbdefghijklnrtuv]{2})*\z/)
|
|
428
|
+
|
|
429
|
+
[value.tr(MODHEX, '0123456789abcdef')].pack('H*')
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
private_class_method def self.serial_number(opts = {})
|
|
433
|
+
status(opts)
|
|
434
|
+
raw = transfer(opts.merge(command: 0x10, data: ''.b, response_length: 4))
|
|
435
|
+
raise ResponseError, 'Device serial is unavailable' unless raw.is_a?(String) && raw.bytesize == 4 && raw.unpack1('N').positive?
|
|
436
|
+
|
|
437
|
+
raw.unpack1('N')
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
private_class_method def self.strict_integer(opts = {})
|
|
441
|
+
value = opts[:value]
|
|
442
|
+
raise ArgumentError, 'Expected an unsigned decimal integer' unless value.is_a?(Integer) || (value.is_a?(String) && value.match?(/\A[0-9]+\z/))
|
|
443
|
+
|
|
444
|
+
value = value.to_i
|
|
445
|
+
raise ArgumentError, 'Numeric option outside its supported range' unless value.between?(opts[:minimum], opts[:maximum])
|
|
446
|
+
|
|
447
|
+
value
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
private_class_method def self.open_export(opts = {})
|
|
451
|
+
path = opts[:path]
|
|
452
|
+
raise ArgumentError, 'config_output must be an explicit nonempty file path' unless path.is_a?(String) && !path.empty? && !path.include?("\0")
|
|
453
|
+
raise DependencyError, 'Secure configuration export requires no-follow file support' unless File.const_defined?(:NOFOLLOW)
|
|
454
|
+
|
|
455
|
+
# Returned handle is owned and closed by provision_yubiotp's ensure.
|
|
456
|
+
file = File.open(path, File::WRONLY | File::CREAT | File::APPEND | File::NOFOLLOW | File::NONBLOCK, 0o600) # rubocop:disable Style/FileOpen
|
|
457
|
+
stat = file.stat
|
|
458
|
+
unless stat.file? && stat.nlink == 1 && stat.uid == Process.euid && stat.mode.nobits?(0o077) && file.flock(File::LOCK_EX | File::LOCK_NB)
|
|
459
|
+
file.close
|
|
460
|
+
raise ArgumentError, 'Configuration export requires an owned private regular file without other links or writers'
|
|
461
|
+
end
|
|
462
|
+
checked = true
|
|
463
|
+
file
|
|
464
|
+
ensure
|
|
465
|
+
file.close if file && !checked && !file.closed?
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
private_class_method def self.decode_secret(opts = {})
|
|
469
|
+
key = opts[:key]
|
|
470
|
+
case opts.fetch(:encoding, :raw).to_s
|
|
471
|
+
when 'raw'
|
|
472
|
+
key
|
|
473
|
+
when 'hex'
|
|
474
|
+
raise ArgumentError, 'Encoded key must contain an even number of hexadecimal digits' unless key.match?(/\A(?:[0-9a-fA-F]{2})+\z/)
|
|
475
|
+
|
|
476
|
+
[key].pack('H*')
|
|
477
|
+
when 'base32'
|
|
478
|
+
text = key.upcase
|
|
479
|
+
raise ArgumentError, 'Encoded key is not canonical Base32' unless text.match?(/\A[A-Z2-7]+={0,6}\z/)
|
|
480
|
+
|
|
481
|
+
bare = text.delete_suffix('=' * text.count('='))
|
|
482
|
+
raise ArgumentError, 'Invalid Base32 key length or padding' unless [0, 2, 4, 5, 7].include?(bare.length % 8) && (!text.include?('=') || (text.length % 8).zero?)
|
|
483
|
+
|
|
484
|
+
alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'
|
|
485
|
+
bits = bare.each_char.map { |char| alphabet.index(char).to_s(2).rjust(5, '0') }.join
|
|
486
|
+
whole = (bits.length / 8) * 8
|
|
487
|
+
raise ArgumentError, 'Base32 key has nonzero unused bits' if bits[whole..].include?('1')
|
|
488
|
+
|
|
489
|
+
[bits[0, whole]].pack('B*')
|
|
490
|
+
else
|
|
491
|
+
raise ArgumentError, 'key_encoding must be raw, hex or base32'
|
|
492
|
+
end
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
private_class_method def self.obtain_secret(opts = {})
|
|
496
|
+
if opts[:generate]
|
|
497
|
+
raise ArgumentError, 'Generation conflicts with the provided secret' if opts[:value]
|
|
498
|
+
raise ArgumentError, 'Generated credentials require an explicit secret_sink callable' unless opts[:secret_sink].respond_to?(:call)
|
|
499
|
+
|
|
500
|
+
value = SecureRandom.random_bytes(opts[:length])
|
|
501
|
+
opts[:secret_sink].call(opts[:name], value.dup)
|
|
502
|
+
value
|
|
503
|
+
else
|
|
504
|
+
secret_value(opts)
|
|
505
|
+
end
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
private_class_method def self.scan_map(opts = {})
|
|
509
|
+
map = KEYBOARD_LAYOUTS[opts[:layout]]
|
|
510
|
+
raise ArgumentError, 'Unsupported keyboard layout' unless map
|
|
511
|
+
|
|
512
|
+
map
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
private_class_method def self.keyboard_flags(opts = {})
|
|
516
|
+
pacing = opts.fetch(:pacing, 0).to_s
|
|
517
|
+
raise ArgumentError, 'pacing must be 0, 20, 40 or 60 milliseconds' unless %w[0 20 40 60].include?(pacing)
|
|
518
|
+
|
|
519
|
+
ext = 0x34
|
|
520
|
+
ext |= 2 if opts[:serial_usb_visible]
|
|
521
|
+
ext |= 8 if opts[:use_numeric_keypad]
|
|
522
|
+
{ ext: ext, tkt: opts[:no_enter] || opts[:enter] == false ? 0 : 0x20, cfg: (pacing.to_i / 20) << 2 }
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
private_class_method def self.parse_arguments(opts = {})
|
|
526
|
+
parsed = Protocol.parse_args(args: opts[:args], flags: opts[:flags].map { |name| "--#{name}" }, values: opts[:values].map { |name| "--#{name}" })
|
|
527
|
+
raise ArgumentError, 'OTP value options cannot be repeated' if parsed[:options].values.any?(Array)
|
|
528
|
+
|
|
529
|
+
parsed
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
public_class_method def self.challenge_response(opts = {})
|
|
533
|
+
challenge = opts[:challenge]
|
|
534
|
+
slot = slot_number(slot: opts.fetch(:slot, 2))
|
|
535
|
+
raise ArgumentError, 'challenge must be a raw String of 1 to 64 bytes' unless challenge.is_a?(String) && challenge.bytesize.between?(1, 64)
|
|
536
|
+
|
|
537
|
+
Protocol.require_confirm(opts) if opts.key?(:confirm)
|
|
538
|
+
state = status(opts)
|
|
539
|
+
require_version(version: state[:version], minimum: [2, 2, 0])
|
|
540
|
+
raise CommandError, 'OTP slot is empty' unless state[:slots][slot][:configured]
|
|
541
|
+
|
|
542
|
+
padding = challenge.end_with?("\0") ? "\1" : "\0"
|
|
543
|
+
response = transfer(opts.merge(command: slot == 1 ? 0x30 : 0x38, data: challenge.b.ljust(64, padding), response_length: 20))
|
|
544
|
+
raise ResponseError, 'OTP response must contain exactly 20 raw bytes' unless response.is_a?(String) && response.bytesize == 20
|
|
545
|
+
|
|
546
|
+
response.b
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
public_class_method def self.provision_challenge_response(opts = {})
|
|
550
|
+
slot = slot_number(slot: opts.fetch(:slot, 2))
|
|
551
|
+
consent(opts.merge(confirm: opts[:confirm]))
|
|
552
|
+
validate_hmac_transport(connection: opts[:connection])
|
|
553
|
+
key = obtain_secret(opts.merge(name: :key, value: opts[:key], length: 20))
|
|
554
|
+
key = decode_secret(key: key, encoding: opts.fetch(:key_encoding, opts[:totp] && !opts[:generate] ? :base32 : :raw))
|
|
555
|
+
key = OpenSSL::Digest::SHA1.digest(key) if key.bytesize > 64
|
|
556
|
+
raise ArgumentError, 'HMAC key must contain 1 to 20 bytes, or more than 64 bytes' unless key.bytesize.between?(1, 20)
|
|
557
|
+
|
|
558
|
+
challenge = opts.fetch(:verification_challenge) { SecureRandom.random_bytes(32) }
|
|
559
|
+
raise ArgumentError, 'verification_challenge must contain 1 to 64 raw bytes' unless challenge.is_a?(String) && challenge.bytesize.between?(1, 64)
|
|
560
|
+
raise ArgumentError, 'touch must be true or false' unless [true, false].include?(opts.fetch(:touch, true))
|
|
561
|
+
|
|
562
|
+
access = access_codes(opts)
|
|
563
|
+
config = pack_config(fixed: ''.b, uid: key.byteslice(16..).to_s.b.ljust(6, "\0"), key: key.byteslice(0, 16).ljust(16, "\0"), access_code: access[:new], ext: 0x24, tkt: 0x40, cfg: opts.fetch(:touch, true) ? 0x2e : 0x26)
|
|
564
|
+
before = status(opts)
|
|
565
|
+
require_version(version: before[:version], minimum: [2, 2, 0])
|
|
566
|
+
result = write_verified(opts.merge(slot: slot, command: slot == 1 ? 1 : 3, data: config + access[:current], before: before, configured: true))
|
|
567
|
+
begin
|
|
568
|
+
response = challenge_response(opts.merge(slot: slot, challenge: challenge))
|
|
569
|
+
expected = OpenSSL::HMAC.digest('SHA1', key, challenge)
|
|
570
|
+
raise ResponseError, 'HMAC readback mismatch' unless OpenSSL.fixed_length_secure_compare(response, expected)
|
|
571
|
+
rescue StandardError
|
|
572
|
+
raise ResponseError, 'OTP provisioning state uncertain: HMAC readback failed; do not automatically reprogram', cause: nil
|
|
573
|
+
end
|
|
574
|
+
opts[:details] ? result.merge(verified: true, verification: :hmac_sha1) : :verified
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
private_class_method def self.write_verified(opts = {})
|
|
578
|
+
before = opts[:before]
|
|
579
|
+
transfer(opts)
|
|
580
|
+
after = status(opts)
|
|
581
|
+
sequence_ok = after[:programming_sequence] == before[:programming_sequence] + 1
|
|
582
|
+
sequence_ok ||= after[:programming_sequence].zero? && before[:programming_sequence].positive? && after[:flags].nobits?(3)
|
|
583
|
+
quirk = opts[:connection].descriptor[:protocol] == :apdu && (after[:version] <=> [5, 0, 0]) >= 0 && (after[:version] <=> [5, 4, 3]).negative? && after[:programming_sequence].zero? && before[:programming_sequence].positive?
|
|
584
|
+
raise ResponseError, 'OTP programming readback is stale' unless before[:version] == after[:version] && (sequence_ok || quirk)
|
|
585
|
+
raise ResponseError, 'OTP slot state readback mismatch' if opts.key?(:configured) && after[:slots][opts[:slot]][:configured] != opts[:configured]
|
|
586
|
+
|
|
587
|
+
{ slot: opts[:slot], status: after, verified: true, verification: sequence_ok ? :programming_sequence_and_slot_state : :firmware_quirk_slot_state, secret_readback: false }
|
|
588
|
+
rescue StandardError
|
|
589
|
+
raise ResponseError, 'OTP write state uncertain: acknowledgment or explicit readback failed; inspect the device, do not replay', cause: nil
|
|
590
|
+
end
|
|
591
|
+
|
|
592
|
+
private_class_method def self.pack_config(opts = {})
|
|
593
|
+
fixed = opts[:fixed]
|
|
594
|
+
body = fixed.ljust(16, "\0") + opts[:uid] + opts[:key] + opts[:access_code] + [fixed.bytesize, opts[:ext], opts[:tkt], opts[:cfg], 0, 0].pack('C*')
|
|
595
|
+
raise ArgumentError, 'Invalid OTP configuration size' unless body.bytesize == 50
|
|
596
|
+
|
|
597
|
+
crc = 0xffff
|
|
598
|
+
body.each_byte do |byte|
|
|
599
|
+
crc ^= byte
|
|
600
|
+
8.times { crc = (crc >> 1) ^ (crc.odd? ? 0x8408 : 0) }
|
|
601
|
+
end
|
|
602
|
+
body + [(~crc) & 0xffff].pack('v')
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
private_class_method def self.secret_value(opts = {})
|
|
606
|
+
value = opts[:value]
|
|
607
|
+
value = opts[:secret_provider].call(opts[:name]) if value.nil? && opts[:secret_provider].respond_to?(:call)
|
|
608
|
+
raise ArgumentError, "#{opts[:name]} must be supplied as nonempty in-memory bytes" unless value.is_a?(String) && !value.empty?
|
|
609
|
+
|
|
610
|
+
value.b
|
|
611
|
+
end
|
|
612
|
+
|
|
613
|
+
private_class_method def self.access_codes(opts = {})
|
|
614
|
+
current = opts[:access_code] || ("\0".b * 6)
|
|
615
|
+
replacement = opts[:new_access_code] || current
|
|
616
|
+
[current, replacement].each do |value|
|
|
617
|
+
raise ArgumentError, 'OTP access codes must contain exactly six raw bytes' unless value.is_a?(String) && value.bytesize == 6
|
|
618
|
+
end
|
|
619
|
+
{ current: current.b, new: replacement.b }
|
|
620
|
+
end
|
|
621
|
+
|
|
622
|
+
private_class_method def self.status(opts = {})
|
|
623
|
+
raw = transfer(opts.merge(command: 0, data: ''.b, response_length: 6))
|
|
624
|
+
raise ResponseError, 'Malformed OTP status' unless raw.is_a?(String) && raw.bytesize == 6
|
|
625
|
+
|
|
626
|
+
version = raw.bytes.take(3)
|
|
627
|
+
flags = raw.byteslice(4, 2).unpack1('v')
|
|
628
|
+
configured = (version <=> [2, 1, 0]) >= 0
|
|
629
|
+
touch = (version <=> [3, 0, 0]) >= 0
|
|
630
|
+
{
|
|
631
|
+
version: version, programming_sequence: raw.getbyte(3), flags: flags,
|
|
632
|
+
slots: {
|
|
633
|
+
1 => { configured: configured ? flags.anybits?(1) : nil, touch_triggered: touch ? flags.anybits?(4) : nil },
|
|
634
|
+
2 => { configured: configured ? flags.anybits?(2) : nil, touch_triggered: touch ? flags.anybits?(8) : nil }
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
end
|
|
638
|
+
|
|
639
|
+
private_class_method def self.validate_hmac_transport(opts = {})
|
|
640
|
+
connection = opts[:connection]
|
|
641
|
+
raise ArgumentError, 'connection must implement descriptor and exchange' unless connection.respond_to?(:descriptor) && connection.respond_to?(:exchange)
|
|
642
|
+
raise CommandError, 'HMAC over USB requires OTP HID; use NFC for smartcard challenge-response' if connection.descriptor[:protocol] == :apdu && connection.descriptor[:transport] == :usb
|
|
643
|
+
|
|
644
|
+
:supported
|
|
645
|
+
end
|
|
646
|
+
|
|
647
|
+
private_class_method def self.validate_nfc(opts = {})
|
|
648
|
+
descriptor = opts[:connection].descriptor
|
|
649
|
+
return :supported if descriptor[:nfc] == true || descriptor[:transport] == :nfc
|
|
650
|
+
raise CommandError, 'NFC is not supported on this device' if descriptor[:nfc] == false
|
|
651
|
+
|
|
652
|
+
information = Management.info(connection: opts[:connection], timeout: opts.fetch(:timeout, 30))
|
|
653
|
+
raise CommandError, 'NFC support is not reported by this device; do not assume every model supports NFC' unless information[:supported_capabilities].key?(:nfc)
|
|
654
|
+
|
|
655
|
+
:supported
|
|
656
|
+
end
|
|
657
|
+
|
|
658
|
+
private_class_method def self.transfer(opts = {})
|
|
659
|
+
connection = opts[:connection]
|
|
660
|
+
raise ArgumentError, 'connection must implement exchange and descriptor' unless connection.respond_to?(:exchange) && connection.respond_to?(:descriptor)
|
|
661
|
+
|
|
662
|
+
timeout = opts.fetch(:timeout, 30)
|
|
663
|
+
raise ArgumentError, 'timeout must be finite positive seconds' unless timeout.is_a?(Numeric) && timeout.finite? && timeout.positive?
|
|
664
|
+
|
|
665
|
+
case connection.descriptor[:protocol]
|
|
666
|
+
when :otp
|
|
667
|
+
connection.exchange(protocol: :otp, command: opts[:command], data: opts[:data], response_length: opts[:response_length], timeout: timeout)
|
|
668
|
+
when :apdu
|
|
669
|
+
if opts[:command].zero?
|
|
670
|
+
Protocol.select(connection: connection, aid: AID, timeout: timeout)
|
|
671
|
+
else
|
|
672
|
+
raise CommandError, 'HMAC challenge-response over USB requires OTP HID; smartcard HMAC is NFC-only' if [0x30, 0x38].include?(opts[:command]) && connection.descriptor[:transport] == :usb
|
|
673
|
+
|
|
674
|
+
Protocol.apdu(connection: connection, ins: 1, p1: opts[:command], data: opts[:data], timeout: timeout)
|
|
675
|
+
end
|
|
676
|
+
else
|
|
677
|
+
raise ArgumentError, 'OTP requires OTP HID or smartcard/NFC transport'
|
|
678
|
+
end
|
|
679
|
+
end
|
|
680
|
+
|
|
681
|
+
private_class_method def self.slot_number(opts = {})
|
|
682
|
+
slot = opts[:slot]
|
|
683
|
+
raise ArgumentError, 'slot must be integer 1 or 2' unless slot.is_a?(Integer) && [1, 2].include?(slot)
|
|
684
|
+
|
|
685
|
+
slot
|
|
686
|
+
end
|
|
687
|
+
|
|
688
|
+
private_class_method def self.require_version(opts = {})
|
|
689
|
+
raise CommandError, 'Operation not supported by this OTP firmware version' unless (opts[:version] <=> opts[:minimum]) >= 0
|
|
690
|
+
|
|
691
|
+
:supported
|
|
692
|
+
end
|
|
693
|
+
|
|
694
|
+
private_class_method def self.consent(opts = {})
|
|
695
|
+
raise ConfirmationRequired, 'This OTP operation requires confirm: true' unless opts[:confirm] == true
|
|
696
|
+
|
|
697
|
+
%i[generate generate_key generate_private_id enter no_enter serial_public_id upload delete_access_code serial_usb_visible use_numeric_keypad touch totp force].each do |key|
|
|
698
|
+
raise ArgumentError, "#{key} must be true or false" if opts.key?(key) && ![true, false].include?(opts[key])
|
|
699
|
+
end
|
|
700
|
+
|
|
701
|
+
:confirmed
|
|
702
|
+
end
|
|
703
|
+
|
|
704
|
+
public_class_method def self.authors(opts = {})
|
|
705
|
+
opts[:format]
|
|
706
|
+
"AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
|
|
707
|
+
end
|
|
708
|
+
|
|
709
|
+
public_class_method def self.help(opts = {})
|
|
710
|
+
opts[:format]
|
|
711
|
+
puts <<~HELP
|
|
712
|
+
Native Ruby only. No subprocess backend. Never log binary responses or secrets.
|
|
713
|
+
Writes are sent once. Uncertain readback raises; inspect rather than replay.
|
|
714
|
+
OTP keyboard/NDEF settings cannot export stored secrets; verification scope is explicit.
|
|
715
|
+
# List implemented OTP leaf operations without opening a device.
|
|
716
|
+
#{self}.operations(
|
|
717
|
+
format: 'optional - reserved output format'
|
|
718
|
+
)
|
|
719
|
+
# Dispatch one native OTP operation using nonsecret arguments only.
|
|
720
|
+
#{self}.execute(
|
|
721
|
+
args: 'optional - Array after otp, empty returns local metadata; credentials belong in memory',
|
|
722
|
+
connection: 'required - exact selected native transport handle; caller owns its lifetime',
|
|
723
|
+
confirm: 'required - literal true for a mutation or cryptographic operation; force does not grant consent',
|
|
724
|
+
timeout: 'optional - finite positive timeout per device exchange, defaults to 30 seconds',
|
|
725
|
+
secret_provider: 'optional - callable receiving a missing secret field Symbol; returns raw secret bytes in memory',
|
|
726
|
+
access_code: 'optional - current six-byte raw slot access code; absence means no supplied access credential',
|
|
727
|
+
new_access_code: 'optional - replacement six-byte raw access code; defaults to preserving the supplied current code',
|
|
728
|
+
secret_sink: 'optional - required callable for generation, receives field Symbol and secret bytes; never log them'
|
|
729
|
+
)
|
|
730
|
+
# Read firmware and slot configuration state without authentication.
|
|
731
|
+
#{self}.info(
|
|
732
|
+
connection: 'required - exact selected native transport handle; caller owns its lifetime',
|
|
733
|
+
timeout: 'optional - finite positive timeout per device exchange, defaults to 30 seconds'
|
|
734
|
+
)
|
|
735
|
+
# Delete the selected slot and verify its empty state.
|
|
736
|
+
#{self}.delete_slot(
|
|
737
|
+
connection: 'required - exact selected native transport handle; caller owns its lifetime',
|
|
738
|
+
confirm: 'required - literal true for a mutation or cryptographic operation; force does not grant consent',
|
|
739
|
+
timeout: 'optional - finite positive timeout per device exchange, defaults to 30 seconds',
|
|
740
|
+
secret_provider: 'optional - callable receiving a missing secret field Symbol; returns raw secret bytes in memory',
|
|
741
|
+
access_code: 'optional - current six-byte raw slot access code; absence means no supplied access credential',
|
|
742
|
+
new_access_code: 'optional - replacement six-byte raw access code; defaults to preserving the supplied current code',
|
|
743
|
+
secret_sink: 'optional - required callable for generation, receives field Symbol and secret bytes; never log them',
|
|
744
|
+
slot: 'required - integer slot 1 or 2'
|
|
745
|
+
)
|
|
746
|
+
# Swap both slot configurations and verify fresh slot state.
|
|
747
|
+
#{self}.swap(
|
|
748
|
+
connection: 'required - exact selected native transport handle; caller owns its lifetime',
|
|
749
|
+
confirm: 'required - literal true for a mutation or cryptographic operation; force does not grant consent',
|
|
750
|
+
timeout: 'optional - finite positive timeout per device exchange, defaults to 30 seconds',
|
|
751
|
+
secret_provider: 'optional - callable receiving a missing secret field Symbol; returns raw secret bytes in memory',
|
|
752
|
+
access_code: 'optional - current six-byte raw slot access code; absence means no supplied access credential',
|
|
753
|
+
new_access_code: 'optional - replacement six-byte raw access code; defaults to preserving the supplied current code',
|
|
754
|
+
secret_sink: 'optional - required callable for generation, receives field Symbol and secret bytes; never log them'
|
|
755
|
+
)
|
|
756
|
+
# Update keyboard and access settings without replacing the stored key.
|
|
757
|
+
#{self}.settings(
|
|
758
|
+
connection: 'required - exact selected native transport handle; caller owns its lifetime',
|
|
759
|
+
confirm: 'required - literal true for a mutation or cryptographic operation; force does not grant consent',
|
|
760
|
+
timeout: 'optional - finite positive timeout per device exchange, defaults to 30 seconds',
|
|
761
|
+
secret_provider: 'optional - callable receiving a missing secret field Symbol; returns raw secret bytes in memory',
|
|
762
|
+
access_code: 'optional - current six-byte raw slot access code; absence means no supplied access credential',
|
|
763
|
+
new_access_code: 'optional - replacement six-byte raw access code; defaults to preserving the supplied current code',
|
|
764
|
+
secret_sink: 'optional - required callable for generation, receives field Symbol and secret bytes; never log them',
|
|
765
|
+
slot: 'required - integer slot 1 or 2',
|
|
766
|
+
delete_access_code: 'optional - true clears protection using the supplied current access code',
|
|
767
|
+
enter: 'optional - append Enter by default; false disables it',
|
|
768
|
+
no_enter: 'optional - true disables appended Enter',
|
|
769
|
+
pacing: 'optional - 0, 20, 40 or 60 milliseconds between emitted keys',
|
|
770
|
+
use_numeric_keypad: 'optional - true selects numeric keypad scan codes',
|
|
771
|
+
serial_usb_visible: 'optional - true exposes serial through the USB descriptor'
|
|
772
|
+
)
|
|
773
|
+
# Provision YubiOTP with in-memory AES material and fresh status readback.
|
|
774
|
+
#{self}.provision_yubiotp(
|
|
775
|
+
connection: 'required - exact selected native transport handle; caller owns its lifetime',
|
|
776
|
+
confirm: 'required - literal true for a mutation or cryptographic operation; force does not grant consent',
|
|
777
|
+
timeout: 'optional - finite positive timeout per device exchange, defaults to 30 seconds',
|
|
778
|
+
secret_provider: 'optional - callable receiving a missing secret field Symbol; returns raw secret bytes in memory',
|
|
779
|
+
access_code: 'optional - current six-byte raw slot access code; absence means no supplied access credential',
|
|
780
|
+
new_access_code: 'optional - replacement six-byte raw access code; defaults to preserving the supplied current code',
|
|
781
|
+
secret_sink: 'optional - required callable for generation, receives field Symbol and secret bytes; never log them',
|
|
782
|
+
slot: 'required - integer slot 1 or 2',
|
|
783
|
+
public_id: 'optional - lowercase even-length modhex public prefix up to 32 characters',
|
|
784
|
+
serial_public_id: 'optional - true derives the prefix from the actual device serial',
|
|
785
|
+
private_id: 'optional - required six raw bytes unless provider or explicit generation supplies them',
|
|
786
|
+
key: 'optional - required sixteen raw AES key bytes unless provider or generation supplies them',
|
|
787
|
+
generate_private_id: 'optional - true generates six private bytes and requires secret_sink',
|
|
788
|
+
generate_key: 'optional - true generates sixteen AES key bytes and requires secret_sink',
|
|
789
|
+
enter: 'optional - append Enter by default',
|
|
790
|
+
no_enter: 'optional - true suppresses appended Enter',
|
|
791
|
+
config_output: 'optional - explicit owned private CSV path; append only after verified programming, no symlinks',
|
|
792
|
+
upload: 'optional - retired upstream flag; true raises before programming and never sends network data'
|
|
793
|
+
)
|
|
794
|
+
# Provision static password scan codes for the selected keyboard layout.
|
|
795
|
+
#{self}.provision_static(
|
|
796
|
+
connection: 'required - exact selected native transport handle; caller owns its lifetime',
|
|
797
|
+
confirm: 'required - literal true for a mutation or cryptographic operation; force does not grant consent',
|
|
798
|
+
timeout: 'optional - finite positive timeout per device exchange, defaults to 30 seconds',
|
|
799
|
+
secret_provider: 'optional - callable receiving a missing secret field Symbol; returns raw secret bytes in memory',
|
|
800
|
+
access_code: 'optional - current six-byte raw slot access code; absence means no supplied access credential',
|
|
801
|
+
new_access_code: 'optional - replacement six-byte raw access code; defaults to preserving the supplied current code',
|
|
802
|
+
secret_sink: 'optional - required callable for generation, receives field Symbol and secret bytes; never log them',
|
|
803
|
+
slot: 'required - integer slot 1 or 2',
|
|
804
|
+
password: 'optional - required in-memory String of 1 to 38 supported characters unless generated or supplied by provider',
|
|
805
|
+
keyboard_layout: 'optional - MODHEX default, US, UK, DE, FR, IT, BEPO or NORMAN',
|
|
806
|
+
length: 'optional - generated password length from 1 through 38, defaults to 38',
|
|
807
|
+
generate: 'optional - true generates a password and requires secret_sink',
|
|
808
|
+
enter: 'optional - append Enter by default',
|
|
809
|
+
no_enter: 'optional - true suppresses appended Enter'
|
|
810
|
+
)
|
|
811
|
+
# Provision a keyboard HOTP credential with explicit counter placement.
|
|
812
|
+
#{self}.provision_hotp(
|
|
813
|
+
connection: 'required - exact selected native transport handle; caller owns its lifetime',
|
|
814
|
+
confirm: 'required - literal true for a mutation or cryptographic operation; force does not grant consent',
|
|
815
|
+
timeout: 'optional - finite positive timeout per device exchange, defaults to 30 seconds',
|
|
816
|
+
secret_provider: 'optional - callable receiving a missing secret field Symbol; returns raw secret bytes in memory',
|
|
817
|
+
access_code: 'optional - current six-byte raw slot access code; absence means no supplied access credential',
|
|
818
|
+
new_access_code: 'optional - replacement six-byte raw access code; defaults to preserving the supplied current code',
|
|
819
|
+
secret_sink: 'optional - required callable for generation, receives field Symbol and secret bytes; never log them',
|
|
820
|
+
slot: 'required - integer slot 1 or 2',
|
|
821
|
+
key: 'optional - required raw HMAC key unless provided in memory by secret_provider',
|
|
822
|
+
digits: 'optional - 6 or 8 decimal output digits, defaults to 6',
|
|
823
|
+
counter: 'optional - initial counter 0 through 1048560, divisible by 16',
|
|
824
|
+
identifier: 'optional - 4, 8 or 12-character modhex/BCD token identifier; omitted means no prefix',
|
|
825
|
+
enter: 'optional - append Enter by default',
|
|
826
|
+
no_enter: 'optional - true suppresses appended Enter'
|
|
827
|
+
)
|
|
828
|
+
# Configure an NFC OTP URI or text record on a supported device.
|
|
829
|
+
#{self}.configure_ndef(
|
|
830
|
+
connection: 'required - exact selected native transport handle; caller owns its lifetime',
|
|
831
|
+
confirm: 'required - literal true for a mutation or cryptographic operation; force does not grant consent',
|
|
832
|
+
timeout: 'optional - finite positive timeout per device exchange, defaults to 30 seconds',
|
|
833
|
+
secret_provider: 'optional - callable receiving a missing secret field Symbol; returns raw secret bytes in memory',
|
|
834
|
+
access_code: 'optional - current six-byte raw slot access code; absence means no supplied access credential',
|
|
835
|
+
new_access_code: 'optional - replacement six-byte raw access code; defaults to preserving the supplied current code',
|
|
836
|
+
secret_sink: 'optional - required callable for generation, receives field Symbol and secret bytes; never log them',
|
|
837
|
+
slot: 'required - integer slot 1 or 2 with an existing credential',
|
|
838
|
+
prefix: 'optional - UTF8 URI or text prefix; URI defaults to the Yubico URL',
|
|
839
|
+
ndef_type: 'optional - URI default or TEXT; complete encoded prefix must fit 54 bytes'
|
|
840
|
+
)
|
|
841
|
+
# Calculate raw challenge bytes or an explicitly requested TOTP code.
|
|
842
|
+
#{self}.calculate(
|
|
843
|
+
connection: 'required - exact selected native transport handle; caller owns its lifetime',
|
|
844
|
+
confirm: 'required - literal true for a mutation or cryptographic operation; force does not grant consent',
|
|
845
|
+
timeout: 'optional - finite positive timeout per device exchange, defaults to 30 seconds',
|
|
846
|
+
secret_provider: 'optional - callable receiving a missing secret field Symbol; returns raw secret bytes in memory',
|
|
847
|
+
access_code: 'optional - current six-byte raw slot access code; absence means no supplied access credential',
|
|
848
|
+
new_access_code: 'optional - replacement six-byte raw access code; defaults to preserving the supplied current code',
|
|
849
|
+
secret_sink: 'optional - required callable for generation, receives field Symbol and secret bytes; never log them',
|
|
850
|
+
slot: 'optional - integer slot 1 or 2, defaults to 2',
|
|
851
|
+
challenge: 'optional - raw 1 to 64-byte input for non-TOTP calculations',
|
|
852
|
+
challenge_argument: 'optional - nonsecret hexadecimal input, or decimal timestamp when totp is true',
|
|
853
|
+
totp: 'optional - true returns a decimal code rather than raw bytes',
|
|
854
|
+
timestamp: 'optional - explicit nonnegative Unix seconds for TOTP, defaults to current time',
|
|
855
|
+
digits: 'optional - 6 or 8 TOTP digits, defaults to 6'
|
|
856
|
+
)
|
|
857
|
+
# Calculate raw HMAC-SHA1 through an already provisioned OTP slot.
|
|
858
|
+
#{self}.challenge_response(
|
|
859
|
+
connection: 'required - exact selected native transport handle; caller owns its lifetime',
|
|
860
|
+
confirm: 'required - literal true for a mutation or cryptographic operation; force does not grant consent',
|
|
861
|
+
timeout: 'optional - finite positive timeout per device exchange, defaults to 30 seconds',
|
|
862
|
+
secret_provider: 'optional - callable receiving a missing secret field Symbol; returns raw secret bytes in memory',
|
|
863
|
+
access_code: 'optional - current six-byte raw slot access code; absence means no supplied access credential',
|
|
864
|
+
new_access_code: 'optional - replacement six-byte raw access code; defaults to preserving the supplied current code',
|
|
865
|
+
secret_sink: 'optional - required callable for generation, receives field Symbol and secret bytes; never log them',
|
|
866
|
+
challenge: 'required - raw binary String of 1 through 64 bytes; return is exactly 20 raw bytes',
|
|
867
|
+
slot: 'optional - integer slot 1 or 2, defaults to 2'
|
|
868
|
+
)
|
|
869
|
+
# Provision HMAC-SHA1 and verify a fresh challenge using native cryptography.
|
|
870
|
+
#{self}.provision_challenge_response(
|
|
871
|
+
connection: 'required - exact selected native transport handle; caller owns its lifetime',
|
|
872
|
+
confirm: 'required - literal true for a mutation or cryptographic operation; force does not grant consent',
|
|
873
|
+
timeout: 'optional - finite positive timeout per device exchange, defaults to 30 seconds',
|
|
874
|
+
secret_provider: 'optional - callable receiving a missing secret field Symbol; returns raw secret bytes in memory',
|
|
875
|
+
access_code: 'optional - current six-byte raw slot access code; absence means no supplied access credential',
|
|
876
|
+
new_access_code: 'optional - replacement six-byte raw access code; defaults to preserving the supplied current code',
|
|
877
|
+
secret_sink: 'optional - required callable for generation, receives field Symbol and secret bytes; never log them',
|
|
878
|
+
slot: 'optional - integer slot 1 or 2, defaults to 2',
|
|
879
|
+
key: 'optional - required raw HMAC material unless provider or explicit generation supplies it',
|
|
880
|
+
key_encoding: 'optional - raw default, hex or base32; never guessed',
|
|
881
|
+
totp: 'optional - true decodes a supplied Base32 key unless key_encoding overrides it',
|
|
882
|
+
generate: 'optional - true generates a 20-byte key and requires secret_sink',
|
|
883
|
+
touch: 'optional - true by default in this typed API; execute chalresp defaults false unless --touch',
|
|
884
|
+
verification_challenge: 'optional - explicit raw 1 to 64-byte test challenge; defaults to 32 random bytes',
|
|
885
|
+
details: 'optional - true returns verification metadata; default returns the legacy verified Symbol'
|
|
886
|
+
)
|
|
887
|
+
# Return module author information.
|
|
888
|
+
#{self}.authors(
|
|
889
|
+
format: 'optional - reserved output format'
|
|
890
|
+
)
|
|
891
|
+
HELP
|
|
892
|
+
nil
|
|
893
|
+
end
|
|
894
|
+
end
|
|
895
|
+
end
|
|
896
|
+
end
|
|
897
|
+
end
|