easy_code_sign 0.1.0
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 +7 -0
- data/CHANGELOG.md +95 -0
- data/LICENSE +21 -0
- data/README.md +331 -0
- data/Rakefile +16 -0
- data/exe/easysign +7 -0
- data/lib/easy_code_sign/cli.rb +428 -0
- data/lib/easy_code_sign/configuration.rb +102 -0
- data/lib/easy_code_sign/deferred_signing_request.rb +104 -0
- data/lib/easy_code_sign/errors.rb +113 -0
- data/lib/easy_code_sign/pdf/appearance_builder.rb +104 -0
- data/lib/easy_code_sign/pdf/timestamp_handler.rb +31 -0
- data/lib/easy_code_sign/providers/base.rb +126 -0
- data/lib/easy_code_sign/providers/pkcs11_base.rb +197 -0
- data/lib/easy_code_sign/providers/safenet.rb +109 -0
- data/lib/easy_code_sign/signable/base.rb +98 -0
- data/lib/easy_code_sign/signable/gem_file.rb +224 -0
- data/lib/easy_code_sign/signable/pdf_file.rb +486 -0
- data/lib/easy_code_sign/signable/zip_file.rb +226 -0
- data/lib/easy_code_sign/signer.rb +254 -0
- data/lib/easy_code_sign/timestamp/client.rb +184 -0
- data/lib/easy_code_sign/timestamp/request.rb +114 -0
- data/lib/easy_code_sign/timestamp/response.rb +246 -0
- data/lib/easy_code_sign/timestamp/verifier.rb +227 -0
- data/lib/easy_code_sign/verification/certificate_chain.rb +298 -0
- data/lib/easy_code_sign/verification/result.rb +222 -0
- data/lib/easy_code_sign/verification/signature_checker.rb +196 -0
- data/lib/easy_code_sign/verification/trust_store.rb +140 -0
- data/lib/easy_code_sign/verifier.rb +426 -0
- data/lib/easy_code_sign/version.rb +5 -0
- data/lib/easy_code_sign.rb +183 -0
- data/plugin/.gitignore +21 -0
- data/plugin/Gemfile +24 -0
- data/plugin/Gemfile.lock +134 -0
- data/plugin/README.md +248 -0
- data/plugin/Rakefile +121 -0
- data/plugin/docs/API_REFERENCE.md +366 -0
- data/plugin/docs/DEVELOPMENT.md +522 -0
- data/plugin/docs/INSTALLATION.md +204 -0
- data/plugin/native_host/build/Rakefile +90 -0
- data/plugin/native_host/install/com.easysign.host.json +9 -0
- data/plugin/native_host/install/install_chrome.sh +81 -0
- data/plugin/native_host/install/install_firefox.sh +81 -0
- data/plugin/native_host/src/easy_sign_host.rb +158 -0
- data/plugin/native_host/src/protocol.rb +101 -0
- data/plugin/native_host/src/signing_service.rb +167 -0
- data/plugin/native_host/test/native_host_test.rb +113 -0
- data/plugin/src/easy_sign/background.rb +323 -0
- data/plugin/src/easy_sign/content.rb +74 -0
- data/plugin/src/easy_sign/inject.rb +239 -0
- data/plugin/src/easy_sign/messaging.rb +109 -0
- data/plugin/src/easy_sign/popup.rb +200 -0
- data/plugin/templates/manifest.json +58 -0
- data/plugin/templates/popup.css +223 -0
- data/plugin/templates/popup.html +59 -0
- data/sig/easy_code_sign.rbs +4 -0
- data/test/easy_code_sign_test.rb +122 -0
- data/test/pdf_signable_test.rb +569 -0
- data/test/signable_test.rb +334 -0
- data/test/test_helper.rb +18 -0
- data/test/timestamp_test.rb +163 -0
- data/test/verification_test.rb +350 -0
- metadata +219 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# backtick_javascript: true
|
|
3
|
+
|
|
4
|
+
require "native"
|
|
5
|
+
require "promise"
|
|
6
|
+
require "easy_sign/messaging"
|
|
7
|
+
|
|
8
|
+
# Injected page script that exposes window.EasySign API
|
|
9
|
+
# This runs in the page context, not the extension context
|
|
10
|
+
module EasySign
|
|
11
|
+
class API
|
|
12
|
+
DEFAULT_TIMEOUT = 120_000 # 2 minutes
|
|
13
|
+
|
|
14
|
+
def initialize
|
|
15
|
+
@pending_requests = {}
|
|
16
|
+
setup_message_listener
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Check if EasySign extension is available and token is connected
|
|
20
|
+
# @return [Promise] Resolves with availability info
|
|
21
|
+
def is_available
|
|
22
|
+
Promise.new do |resolve, reject|
|
|
23
|
+
request = create_request(Messaging::Types::AVAILABILITY_REQUEST, {})
|
|
24
|
+
send_request(request, resolve, reject, 10_000) # 10 second timeout for availability check
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Sign a PDF document
|
|
29
|
+
# @param pdf_blob [Blob] The PDF file as a Blob
|
|
30
|
+
# @param options [Hash] Signing options
|
|
31
|
+
# @option options [String] :reason Reason for signing
|
|
32
|
+
# @option options [String] :location Location of signing
|
|
33
|
+
# @option options [Boolean] :visible_signature Add visible signature annotation
|
|
34
|
+
# @option options [String] :signature_position Position (top_left, top_right, bottom_left, bottom_right)
|
|
35
|
+
# @option options [Integer] :signature_page Page number for signature
|
|
36
|
+
# @option options [Boolean] :timestamp Add RFC 3161 timestamp
|
|
37
|
+
# @return [Promise] Resolves with signed PDF Blob
|
|
38
|
+
def sign(pdf_blob, options = {})
|
|
39
|
+
Promise.new do |resolve, reject|
|
|
40
|
+
# Convert Blob to Base64
|
|
41
|
+
blob_to_base64(pdf_blob).then do |base64_data|
|
|
42
|
+
request = create_request(
|
|
43
|
+
Messaging::Types::SIGN_REQUEST,
|
|
44
|
+
{ pdf_data: base64_data, options: normalize_options(options) }
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# Wrap resolve to convert Base64 back to Blob
|
|
48
|
+
wrapped_resolve = ->(response) {
|
|
49
|
+
if response[:payload] && response[:payload][:signedPdfData]
|
|
50
|
+
base64_to_blob(response[:payload][:signedPdfData], "application/pdf").then do |blob|
|
|
51
|
+
resolve.call({
|
|
52
|
+
blob: blob,
|
|
53
|
+
signer_name: response[:payload][:signerName],
|
|
54
|
+
signed_at: response[:payload][:signedAt],
|
|
55
|
+
timestamped: response[:payload][:timestamped]
|
|
56
|
+
})
|
|
57
|
+
end
|
|
58
|
+
else
|
|
59
|
+
resolve.call(response)
|
|
60
|
+
end
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
send_request(request, wrapped_resolve, reject)
|
|
64
|
+
end.fail do |error|
|
|
65
|
+
reject.call(error)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Verify a signed PDF document
|
|
71
|
+
# @param pdf_blob [Blob] The signed PDF file as a Blob
|
|
72
|
+
# @param options [Hash] Verification options
|
|
73
|
+
# @option options [Boolean] :check_timestamp Verify timestamp (default: true)
|
|
74
|
+
# @return [Promise] Resolves with verification result
|
|
75
|
+
def verify(pdf_blob, options = {})
|
|
76
|
+
Promise.new do |resolve, reject|
|
|
77
|
+
blob_to_base64(pdf_blob).then do |base64_data|
|
|
78
|
+
request = create_request(
|
|
79
|
+
Messaging::Types::VERIFY_REQUEST,
|
|
80
|
+
{
|
|
81
|
+
pdf_data: base64_data,
|
|
82
|
+
check_timestamp: options[:check_timestamp] != false
|
|
83
|
+
}
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
send_request(request, resolve, reject)
|
|
87
|
+
end.fail do |error|
|
|
88
|
+
reject.call(error)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Cancel an ongoing signing operation
|
|
94
|
+
# @param request_id [String] The request ID to cancel
|
|
95
|
+
# @return [Promise]
|
|
96
|
+
def cancel(request_id)
|
|
97
|
+
Promise.new do |resolve, reject|
|
|
98
|
+
request = {
|
|
99
|
+
type: Messaging::Types::CANCEL_REQUEST,
|
|
100
|
+
request_id: request_id,
|
|
101
|
+
target: Messaging::Targets::EXTENSION
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
`window.postMessage(#{request.to_n}, '*')`
|
|
105
|
+
resolve.call({ cancelled: true })
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
private
|
|
110
|
+
|
|
111
|
+
def setup_message_listener
|
|
112
|
+
`window.addEventListener('message', (event) => {
|
|
113
|
+
if (event.source !== window) return;
|
|
114
|
+
if (!event.data || event.data.target !== #{Messaging::Targets::PAGE.to_n}) return;
|
|
115
|
+
|
|
116
|
+
#{handle_response(`event.data`)}
|
|
117
|
+
})`
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def handle_response(data)
|
|
121
|
+
request_id = data[:request_id] || `data.requestId` || `data.request_id`
|
|
122
|
+
callbacks = @pending_requests.delete(request_id)
|
|
123
|
+
|
|
124
|
+
return unless callbacks
|
|
125
|
+
|
|
126
|
+
resolve = callbacks[:resolve]
|
|
127
|
+
reject = callbacks[:reject]
|
|
128
|
+
|
|
129
|
+
# Clear timeout
|
|
130
|
+
`clearTimeout(#{callbacks[:timeout_id]})` if callbacks[:timeout_id]
|
|
131
|
+
|
|
132
|
+
error = data[:error] || `data.error`
|
|
133
|
+
if error
|
|
134
|
+
error_obj = `new Error(#{error[:message] || `error.message` || 'Unknown error'})`
|
|
135
|
+
`#{error_obj}.code = #{error[:code] || `error.code`}`
|
|
136
|
+
reject.call(error_obj)
|
|
137
|
+
else
|
|
138
|
+
resolve.call(data)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def create_request(type, payload)
|
|
143
|
+
{
|
|
144
|
+
type: type,
|
|
145
|
+
request_id: generate_request_id,
|
|
146
|
+
payload: payload,
|
|
147
|
+
target: Messaging::Targets::EXTENSION
|
|
148
|
+
}
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def send_request(request, resolve, reject, timeout = DEFAULT_TIMEOUT)
|
|
152
|
+
request_id = request[:request_id]
|
|
153
|
+
|
|
154
|
+
# Set up timeout
|
|
155
|
+
timeout_id = `setTimeout(() => {
|
|
156
|
+
#{handle_timeout(request_id)}
|
|
157
|
+
}, #{timeout})`
|
|
158
|
+
|
|
159
|
+
@pending_requests[request_id] = {
|
|
160
|
+
resolve: resolve,
|
|
161
|
+
reject: reject,
|
|
162
|
+
timeout_id: timeout_id
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
`window.postMessage(#{request.to_n}, '*')`
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def handle_timeout(request_id)
|
|
169
|
+
callbacks = @pending_requests.delete(request_id)
|
|
170
|
+
return unless callbacks
|
|
171
|
+
|
|
172
|
+
error = `new Error('Operation timed out')`
|
|
173
|
+
`#{error}.code = 'TIMEOUT'`
|
|
174
|
+
callbacks[:reject].call(error)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def generate_request_id
|
|
178
|
+
`crypto.randomUUID ? crypto.randomUUID() :
|
|
179
|
+
Date.now().toString(36) + Math.random().toString(36).substr(2)`
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def normalize_options(options)
|
|
183
|
+
# Convert Ruby-style options to camelCase for native host
|
|
184
|
+
{
|
|
185
|
+
reason: options[:reason],
|
|
186
|
+
location: options[:location],
|
|
187
|
+
visibleSignature: options[:visible_signature],
|
|
188
|
+
signaturePosition: options[:signature_position],
|
|
189
|
+
signaturePage: options[:signature_page],
|
|
190
|
+
timestamp: options[:timestamp],
|
|
191
|
+
timestampAuthority: options[:timestamp_authority]
|
|
192
|
+
}.compact
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def blob_to_base64(blob)
|
|
196
|
+
Promise.new do |resolve, reject|
|
|
197
|
+
`const reader = new FileReader();
|
|
198
|
+
reader.onload = function() {
|
|
199
|
+
// Remove data URL prefix to get just base64
|
|
200
|
+
const base64 = reader.result.split(',')[1];
|
|
201
|
+
#{resolve.call(`base64`)}
|
|
202
|
+
};
|
|
203
|
+
reader.onerror = function() {
|
|
204
|
+
#{reject.call(`reader.error`)}
|
|
205
|
+
};
|
|
206
|
+
reader.readAsDataURL(#{blob});`
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def base64_to_blob(base64, mime_type)
|
|
211
|
+
Promise.new do |resolve, _reject|
|
|
212
|
+
`const byteCharacters = atob(#{base64});
|
|
213
|
+
const byteNumbers = new Array(byteCharacters.length);
|
|
214
|
+
for (let i = 0; i < byteCharacters.length; i++) {
|
|
215
|
+
byteNumbers[i] = byteCharacters.charCodeAt(i);
|
|
216
|
+
}
|
|
217
|
+
const byteArray = new Uint8Array(byteNumbers);
|
|
218
|
+
const blob = new Blob([byteArray], { type: #{mime_type} });
|
|
219
|
+
#{resolve.call(`blob`)};`
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Expose to window object
|
|
226
|
+
`window.EasySign = #{EasySign::API.new.to_n}`
|
|
227
|
+
|
|
228
|
+
# Also expose individual methods directly for convenience
|
|
229
|
+
`window.EasySign.sign = function(blob, options) {
|
|
230
|
+
return #{EasySign::API.new}.sign(blob, options || {});
|
|
231
|
+
};
|
|
232
|
+
window.EasySign.verify = function(blob, options) {
|
|
233
|
+
return #{EasySign::API.new}.verify(blob, options || {});
|
|
234
|
+
};
|
|
235
|
+
window.EasySign.isAvailable = function() {
|
|
236
|
+
return #{EasySign::API.new}.is_available();
|
|
237
|
+
};`
|
|
238
|
+
|
|
239
|
+
puts "EasySign API injected into page"
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# backtick_javascript: true
|
|
3
|
+
|
|
4
|
+
# Message protocol constants and helpers for EasySign browser extension
|
|
5
|
+
module EasySign
|
|
6
|
+
module Messaging
|
|
7
|
+
# Message types
|
|
8
|
+
module Types
|
|
9
|
+
# Requests from page/content script to background
|
|
10
|
+
SIGN_REQUEST = "sign_request"
|
|
11
|
+
VERIFY_REQUEST = "verify_request"
|
|
12
|
+
AVAILABILITY_REQUEST = "availability_request"
|
|
13
|
+
CANCEL_REQUEST = "cancel_request"
|
|
14
|
+
|
|
15
|
+
# Responses from background to page/content script
|
|
16
|
+
SIGN_RESPONSE = "sign_response"
|
|
17
|
+
VERIFY_RESPONSE = "verify_response"
|
|
18
|
+
AVAILABILITY_RESPONSE = "availability_response"
|
|
19
|
+
ERROR_RESPONSE = "error"
|
|
20
|
+
|
|
21
|
+
# Internal extension messages
|
|
22
|
+
PIN_SUBMITTED = "pin_submitted"
|
|
23
|
+
PIN_CANCELLED = "pin_cancelled"
|
|
24
|
+
POPUP_READY = "popup_ready"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Error codes matching native host protocol
|
|
28
|
+
module ErrorCodes
|
|
29
|
+
TOKEN_NOT_FOUND = "TOKEN_NOT_FOUND"
|
|
30
|
+
PIN_INCORRECT = "PIN_INCORRECT"
|
|
31
|
+
TOKEN_LOCKED = "TOKEN_LOCKED"
|
|
32
|
+
INVALID_PDF = "INVALID_PDF"
|
|
33
|
+
SIGNING_FAILED = "SIGNING_FAILED"
|
|
34
|
+
VERIFICATION_FAILED = "VERIFICATION_FAILED"
|
|
35
|
+
NATIVE_HOST_NOT_FOUND = "NATIVE_HOST_NOT_FOUND"
|
|
36
|
+
TIMEOUT = "TIMEOUT"
|
|
37
|
+
CANCELLED = "CANCELLED"
|
|
38
|
+
ORIGIN_NOT_ALLOWED = "ORIGIN_NOT_ALLOWED"
|
|
39
|
+
INTERNAL_ERROR = "INTERNAL_ERROR"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Message targets for postMessage routing
|
|
43
|
+
module Targets
|
|
44
|
+
EXTENSION = "easy-sign-extension"
|
|
45
|
+
PAGE = "easy-sign-page"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Default timeout for signing operations (ms)
|
|
49
|
+
DEFAULT_TIMEOUT = 120_000 # 2 minutes
|
|
50
|
+
|
|
51
|
+
# Generate unique request ID
|
|
52
|
+
def self.generate_request_id
|
|
53
|
+
# Use crypto.randomUUID if available, fallback to timestamp + random
|
|
54
|
+
`typeof crypto !== 'undefined' && crypto.randomUUID ?
|
|
55
|
+
crypto.randomUUID() :
|
|
56
|
+
Date.now().toString(36) + Math.random().toString(36).substr(2)`
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Create a request message
|
|
60
|
+
def self.create_request(type, payload = {})
|
|
61
|
+
{
|
|
62
|
+
type: type,
|
|
63
|
+
request_id: generate_request_id,
|
|
64
|
+
payload: payload,
|
|
65
|
+
timestamp: `Date.now()`
|
|
66
|
+
}
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Create a response message
|
|
70
|
+
def self.create_response(request_id, type, payload = nil, error = nil)
|
|
71
|
+
{
|
|
72
|
+
type: type,
|
|
73
|
+
request_id: request_id,
|
|
74
|
+
payload: payload,
|
|
75
|
+
error: error,
|
|
76
|
+
timestamp: `Date.now()`
|
|
77
|
+
}
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Create an error response
|
|
81
|
+
def self.create_error(request_id, code, message, details = nil)
|
|
82
|
+
create_response(
|
|
83
|
+
request_id,
|
|
84
|
+
Types::ERROR_RESPONSE,
|
|
85
|
+
nil,
|
|
86
|
+
{ code: code, message: message, details: details }
|
|
87
|
+
)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Human-readable error messages
|
|
91
|
+
ERROR_MESSAGES = {
|
|
92
|
+
ErrorCodes::TOKEN_NOT_FOUND => "Hardware token not found. Please connect your token.",
|
|
93
|
+
ErrorCodes::PIN_INCORRECT => "Incorrect PIN entered.",
|
|
94
|
+
ErrorCodes::TOKEN_LOCKED => "Token is locked. Please contact your administrator.",
|
|
95
|
+
ErrorCodes::INVALID_PDF => "The PDF file is invalid or corrupted.",
|
|
96
|
+
ErrorCodes::SIGNING_FAILED => "Failed to sign the document.",
|
|
97
|
+
ErrorCodes::VERIFICATION_FAILED => "Failed to verify the signature.",
|
|
98
|
+
ErrorCodes::NATIVE_HOST_NOT_FOUND => "EasySign native host not installed.",
|
|
99
|
+
ErrorCodes::TIMEOUT => "Operation timed out.",
|
|
100
|
+
ErrorCodes::CANCELLED => "Operation was cancelled.",
|
|
101
|
+
ErrorCodes::ORIGIN_NOT_ALLOWED => "This website is not allowed to use EasySign.",
|
|
102
|
+
ErrorCodes::INTERNAL_ERROR => "An internal error occurred."
|
|
103
|
+
}.freeze
|
|
104
|
+
|
|
105
|
+
def self.error_message(code)
|
|
106
|
+
ERROR_MESSAGES[code] || "Unknown error: #{code}"
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# backtick_javascript: true
|
|
3
|
+
|
|
4
|
+
require "native"
|
|
5
|
+
require "easy_sign/messaging"
|
|
6
|
+
|
|
7
|
+
# Popup controller for PIN entry
|
|
8
|
+
module EasySign
|
|
9
|
+
class Popup
|
|
10
|
+
def initialize
|
|
11
|
+
@port = nil
|
|
12
|
+
@signing_context = nil
|
|
13
|
+
setup_ui
|
|
14
|
+
connect_to_background
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def setup_ui
|
|
18
|
+
# Wait for DOM to be ready
|
|
19
|
+
`document.addEventListener('DOMContentLoaded', () => {
|
|
20
|
+
#{init_ui}
|
|
21
|
+
})`
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def init_ui
|
|
25
|
+
# Get UI elements
|
|
26
|
+
@pin_input = `document.getElementById('pin-input')`
|
|
27
|
+
@submit_btn = `document.getElementById('submit-btn')`
|
|
28
|
+
@cancel_btn = `document.getElementById('cancel-btn')`
|
|
29
|
+
@status_text = `document.getElementById('status-text')`
|
|
30
|
+
@origin_text = `document.getElementById('origin-text')`
|
|
31
|
+
@error_text = `document.getElementById('error-text')`
|
|
32
|
+
|
|
33
|
+
setup_event_listeners
|
|
34
|
+
focus_pin_input
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def setup_event_listeners
|
|
38
|
+
# Submit button click
|
|
39
|
+
`#{@submit_btn}.addEventListener('click', () => {
|
|
40
|
+
#{submit_pin}
|
|
41
|
+
})`
|
|
42
|
+
|
|
43
|
+
# Cancel button click
|
|
44
|
+
`#{@cancel_btn}.addEventListener('click', () => {
|
|
45
|
+
#{cancel_signing}
|
|
46
|
+
})`
|
|
47
|
+
|
|
48
|
+
# Enter key in PIN input
|
|
49
|
+
`#{@pin_input}.addEventListener('keypress', (e) => {
|
|
50
|
+
if (e.key === 'Enter') {
|
|
51
|
+
#{submit_pin}
|
|
52
|
+
}
|
|
53
|
+
})`
|
|
54
|
+
|
|
55
|
+
# Clear error on input
|
|
56
|
+
`#{@pin_input}.addEventListener('input', () => {
|
|
57
|
+
#{clear_error}
|
|
58
|
+
})`
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def connect_to_background
|
|
62
|
+
@port = `chrome.runtime.connect({ name: 'popup' })`
|
|
63
|
+
|
|
64
|
+
`#{@port}.onMessage.addListener((message) => {
|
|
65
|
+
#{handle_background_message(`message`)}
|
|
66
|
+
})`
|
|
67
|
+
|
|
68
|
+
`#{@port}.onDisconnect.addListener(() => {
|
|
69
|
+
#{handle_disconnect}
|
|
70
|
+
})`
|
|
71
|
+
|
|
72
|
+
# Notify background that popup is ready
|
|
73
|
+
send_to_background({ type: Messaging::Types::POPUP_READY })
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def handle_background_message(message)
|
|
77
|
+
type = message[:type] || `message.type`
|
|
78
|
+
|
|
79
|
+
case type
|
|
80
|
+
when "signing_context"
|
|
81
|
+
handle_signing_context(message)
|
|
82
|
+
when "error"
|
|
83
|
+
show_error(message[:error] || `message.error`)
|
|
84
|
+
when "pin_incorrect"
|
|
85
|
+
show_error("Incorrect PIN. Please try again.")
|
|
86
|
+
clear_pin
|
|
87
|
+
focus_pin_input
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def handle_signing_context(context)
|
|
92
|
+
@signing_context = context
|
|
93
|
+
origin = context[:origin] || `context.origin`
|
|
94
|
+
options = context[:options] || `context.options` || {}
|
|
95
|
+
|
|
96
|
+
# Update UI with signing context
|
|
97
|
+
if origin && @origin_text
|
|
98
|
+
`#{@origin_text}.textContent = #{origin}`
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Show what will be signed
|
|
102
|
+
if options[:reason] && @status_text
|
|
103
|
+
`#{@status_text}.textContent = 'Reason: ' + #{options[:reason]}`
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def handle_disconnect
|
|
108
|
+
# Background disconnected, close popup
|
|
109
|
+
`window.close()`
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def submit_pin
|
|
113
|
+
pin = `#{@pin_input}.value`
|
|
114
|
+
|
|
115
|
+
# Validate PIN
|
|
116
|
+
if pin.nil? || `#{pin}.length === 0`
|
|
117
|
+
show_error("Please enter your PIN")
|
|
118
|
+
return
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
if `#{pin}.length < 4`
|
|
122
|
+
show_error("PIN must be at least 4 characters")
|
|
123
|
+
return
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Disable UI while processing
|
|
127
|
+
disable_ui
|
|
128
|
+
show_status("Signing document...")
|
|
129
|
+
|
|
130
|
+
# Send PIN to background
|
|
131
|
+
send_to_background({
|
|
132
|
+
type: Messaging::Types::PIN_SUBMITTED,
|
|
133
|
+
pin: pin
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
# Clear PIN from memory
|
|
137
|
+
clear_pin
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def cancel_signing
|
|
141
|
+
send_to_background({ type: Messaging::Types::PIN_CANCELLED })
|
|
142
|
+
`window.close()`
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def send_to_background(message)
|
|
146
|
+
return unless @port
|
|
147
|
+
|
|
148
|
+
`#{@port}.postMessage(#{message.to_n})`
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def show_error(message)
|
|
152
|
+
return unless @error_text
|
|
153
|
+
|
|
154
|
+
`#{@error_text}.textContent = #{message}`
|
|
155
|
+
`#{@error_text}.style.display = 'block'`
|
|
156
|
+
enable_ui
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def clear_error
|
|
160
|
+
return unless @error_text
|
|
161
|
+
|
|
162
|
+
`#{@error_text}.textContent = ''`
|
|
163
|
+
`#{@error_text}.style.display = 'none'`
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def show_status(message)
|
|
167
|
+
return unless @status_text
|
|
168
|
+
|
|
169
|
+
`#{@status_text}.textContent = #{message}`
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def clear_pin
|
|
173
|
+
return unless @pin_input
|
|
174
|
+
|
|
175
|
+
`#{@pin_input}.value = ''`
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def focus_pin_input
|
|
179
|
+
return unless @pin_input
|
|
180
|
+
|
|
181
|
+
`#{@pin_input}.focus()`
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def disable_ui
|
|
185
|
+
`#{@pin_input}.disabled = true` if @pin_input
|
|
186
|
+
`#{@submit_btn}.disabled = true` if @submit_btn
|
|
187
|
+
`#{@cancel_btn}.disabled = true` if @cancel_btn
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def enable_ui
|
|
191
|
+
`#{@pin_input}.disabled = false` if @pin_input
|
|
192
|
+
`#{@submit_btn}.disabled = false` if @submit_btn
|
|
193
|
+
`#{@cancel_btn}.disabled = false` if @cancel_btn
|
|
194
|
+
focus_pin_input
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Initialize popup
|
|
200
|
+
EasySign::Popup.new
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"manifest_version": 3,
|
|
3
|
+
"name": "EasySign PDF Signer",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"description": "Sign PDF documents using hardware security tokens (HSM/smart cards)",
|
|
6
|
+
|
|
7
|
+
"permissions": [
|
|
8
|
+
"nativeMessaging",
|
|
9
|
+
"storage"
|
|
10
|
+
],
|
|
11
|
+
|
|
12
|
+
"host_permissions": [
|
|
13
|
+
"https://*/*",
|
|
14
|
+
"http://localhost/*"
|
|
15
|
+
],
|
|
16
|
+
|
|
17
|
+
"background": {
|
|
18
|
+
"service_worker": "background.js",
|
|
19
|
+
"type": "module"
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
"content_scripts": [
|
|
23
|
+
{
|
|
24
|
+
"matches": ["https://*/*", "http://localhost/*"],
|
|
25
|
+
"js": ["content.js"],
|
|
26
|
+
"run_at": "document_start",
|
|
27
|
+
"all_frames": false
|
|
28
|
+
}
|
|
29
|
+
],
|
|
30
|
+
|
|
31
|
+
"web_accessible_resources": [
|
|
32
|
+
{
|
|
33
|
+
"resources": ["inject.js"],
|
|
34
|
+
"matches": ["https://*/*", "http://localhost/*"]
|
|
35
|
+
}
|
|
36
|
+
],
|
|
37
|
+
|
|
38
|
+
"action": {
|
|
39
|
+
"default_popup": "popup/popup.html",
|
|
40
|
+
"default_icon": {
|
|
41
|
+
"16": "icons/icon16.png",
|
|
42
|
+
"48": "icons/icon48.png",
|
|
43
|
+
"128": "icons/icon128.png"
|
|
44
|
+
},
|
|
45
|
+
"default_title": "EasySign PDF Signer"
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
"icons": {
|
|
49
|
+
"16": "icons/icon16.png",
|
|
50
|
+
"48": "icons/icon48.png",
|
|
51
|
+
"128": "icons/icon128.png"
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
"options_ui": {
|
|
55
|
+
"page": "popup/popup.html",
|
|
56
|
+
"open_in_tab": false
|
|
57
|
+
}
|
|
58
|
+
}
|