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,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "minitest/autorun"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
# Add paths for testing
|
|
7
|
+
$LOAD_PATH.unshift(File.expand_path("../../src", __dir__))
|
|
8
|
+
$LOAD_PATH.unshift(File.expand_path("../../../lib", __dir__))
|
|
9
|
+
|
|
10
|
+
require "protocol"
|
|
11
|
+
|
|
12
|
+
class ProtocolTest < Minitest::Test
|
|
13
|
+
def test_sign_response_creates_correct_structure
|
|
14
|
+
result = {
|
|
15
|
+
signed_pdf_data: "base64data",
|
|
16
|
+
signer_name: "CN=Test User",
|
|
17
|
+
signed_at: Time.now,
|
|
18
|
+
timestamped: true
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
response = EasySign::Protocol.sign_response("req-123", result)
|
|
22
|
+
|
|
23
|
+
assert_equal "sign_response", response[:type]
|
|
24
|
+
assert_equal "req-123", response[:requestId]
|
|
25
|
+
assert_nil response[:error]
|
|
26
|
+
assert_equal "base64data", response[:payload][:signedPdfData]
|
|
27
|
+
assert_equal "CN=Test User", response[:payload][:signerName]
|
|
28
|
+
assert response[:payload][:timestamped]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def test_error_response_creates_correct_structure
|
|
32
|
+
response = EasySign::Protocol.error_response(
|
|
33
|
+
"req-456",
|
|
34
|
+
EasySign::Protocol::ErrorCodes::PIN_INCORRECT,
|
|
35
|
+
"Wrong PIN",
|
|
36
|
+
{ retriesRemaining: 2 }
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
assert_equal "error", response[:type]
|
|
40
|
+
assert_equal "req-456", response[:requestId]
|
|
41
|
+
assert_nil response[:payload]
|
|
42
|
+
assert_equal "PIN_INCORRECT", response[:error][:code]
|
|
43
|
+
assert_equal "Wrong PIN", response[:error][:message]
|
|
44
|
+
assert_equal 2, response[:error][:details][:retriesRemaining]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def test_verify_response_creates_correct_structure
|
|
48
|
+
result = {
|
|
49
|
+
valid: true,
|
|
50
|
+
signer_name: "CN=Test User",
|
|
51
|
+
signature_valid: true,
|
|
52
|
+
integrity_valid: true,
|
|
53
|
+
certificate_valid: true,
|
|
54
|
+
chain_valid: true,
|
|
55
|
+
trusted: true,
|
|
56
|
+
timestamped: false,
|
|
57
|
+
errors: [],
|
|
58
|
+
warnings: []
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
response = EasySign::Protocol.verify_response("req-789", result)
|
|
62
|
+
|
|
63
|
+
assert_equal "verify_response", response[:type]
|
|
64
|
+
assert_equal "req-789", response[:requestId]
|
|
65
|
+
assert response[:payload][:valid]
|
|
66
|
+
assert response[:payload][:signatureValid]
|
|
67
|
+
assert_empty response[:payload][:errors]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def test_availability_response_creates_correct_structure
|
|
71
|
+
result = {
|
|
72
|
+
available: true,
|
|
73
|
+
token_present: true,
|
|
74
|
+
slots: [
|
|
75
|
+
{ index: 0, token_label: "Token1", manufacturer: "SafeNet", serial: "12345" }
|
|
76
|
+
]
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
response = EasySign::Protocol.availability_response("req-000", result)
|
|
80
|
+
|
|
81
|
+
assert_equal "availability_response", response[:type]
|
|
82
|
+
assert response[:payload][:available]
|
|
83
|
+
assert response[:payload][:tokenPresent]
|
|
84
|
+
assert_equal 1, response[:payload][:slots].length
|
|
85
|
+
assert_equal "Token1", response[:payload][:slots][0][:tokenLabel]
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
class MessageFormatTest < Minitest::Test
|
|
90
|
+
def test_native_message_encoding
|
|
91
|
+
message = { type: "test", requestId: "123" }
|
|
92
|
+
json = message.to_json
|
|
93
|
+
length = [json.bytesize].pack("V")
|
|
94
|
+
|
|
95
|
+
# Verify length prefix format
|
|
96
|
+
assert_equal 4, length.bytesize
|
|
97
|
+
assert_equal json.bytesize, length.unpack1("V")
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def test_native_message_decoding
|
|
101
|
+
message = { type: "sign", requestId: "abc", payload: { pdfData: "test" } }
|
|
102
|
+
json = message.to_json
|
|
103
|
+
length_bytes = [json.bytesize].pack("V")
|
|
104
|
+
|
|
105
|
+
# Simulate reading
|
|
106
|
+
decoded_length = length_bytes.unpack1("V")
|
|
107
|
+
assert_equal json.bytesize, decoded_length
|
|
108
|
+
|
|
109
|
+
decoded = JSON.parse(json, symbolize_names: true)
|
|
110
|
+
assert_equal "sign", decoded[:type]
|
|
111
|
+
assert_equal "abc", decoded[:requestId]
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# backtick_javascript: true
|
|
3
|
+
|
|
4
|
+
require "native"
|
|
5
|
+
require "easy_sign/messaging"
|
|
6
|
+
|
|
7
|
+
# Background service worker for EasySign browser extension
|
|
8
|
+
# Handles communication between content scripts and native messaging host
|
|
9
|
+
module EasySign
|
|
10
|
+
class Background
|
|
11
|
+
NATIVE_HOST_NAME = "com.easysign.host"
|
|
12
|
+
|
|
13
|
+
def initialize
|
|
14
|
+
@native_port = nil
|
|
15
|
+
@pending_requests = {}
|
|
16
|
+
@current_signing_request = nil
|
|
17
|
+
setup_listeners
|
|
18
|
+
puts "EasySign background service worker initialized"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def setup_listeners
|
|
22
|
+
# Listen for messages from content scripts
|
|
23
|
+
`chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|
24
|
+
#{handle_message(`message`, `sender`, `sendResponse`)}
|
|
25
|
+
return true; // Keep channel open for async response
|
|
26
|
+
})`
|
|
27
|
+
|
|
28
|
+
# Listen for connections (for popup communication)
|
|
29
|
+
`chrome.runtime.onConnect.addListener((port) => {
|
|
30
|
+
#{handle_port_connect(`port`)}
|
|
31
|
+
})`
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def handle_message(message, sender, send_response)
|
|
35
|
+
type = message[:type] || `message.type`
|
|
36
|
+
request_id = message[:request_id] || `message.requestId` || `message.request_id`
|
|
37
|
+
|
|
38
|
+
case type
|
|
39
|
+
when Messaging::Types::SIGN_REQUEST
|
|
40
|
+
handle_sign_request(message, sender, send_response)
|
|
41
|
+
when Messaging::Types::VERIFY_REQUEST
|
|
42
|
+
handle_verify_request(message, sender, send_response)
|
|
43
|
+
when Messaging::Types::AVAILABILITY_REQUEST
|
|
44
|
+
handle_availability_request(send_response)
|
|
45
|
+
when Messaging::Types::CANCEL_REQUEST
|
|
46
|
+
handle_cancel_request(request_id, send_response)
|
|
47
|
+
else
|
|
48
|
+
send_error(send_response, request_id, Messaging::ErrorCodes::INTERNAL_ERROR,
|
|
49
|
+
"Unknown message type: #{type}")
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def handle_port_connect(port)
|
|
54
|
+
port_name = `port.name`
|
|
55
|
+
|
|
56
|
+
case port_name
|
|
57
|
+
when "popup"
|
|
58
|
+
setup_popup_port(port)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def setup_popup_port(port)
|
|
63
|
+
@popup_port = port
|
|
64
|
+
|
|
65
|
+
`port.onMessage.addListener((message) => {
|
|
66
|
+
#{handle_popup_message(`message`)}
|
|
67
|
+
})`
|
|
68
|
+
|
|
69
|
+
`port.onDisconnect.addListener(() => {
|
|
70
|
+
#{handle_popup_disconnect}
|
|
71
|
+
})`
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def handle_popup_message(message)
|
|
75
|
+
type = message[:type] || `message.type`
|
|
76
|
+
|
|
77
|
+
case type
|
|
78
|
+
when Messaging::Types::PIN_SUBMITTED
|
|
79
|
+
pin = message[:pin] || `message.pin`
|
|
80
|
+
complete_signing_with_pin(pin)
|
|
81
|
+
when Messaging::Types::PIN_CANCELLED
|
|
82
|
+
cancel_current_signing
|
|
83
|
+
when Messaging::Types::POPUP_READY
|
|
84
|
+
send_signing_context_to_popup
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def handle_popup_disconnect
|
|
89
|
+
@popup_port = nil
|
|
90
|
+
# If popup closes without submitting PIN, cancel the operation
|
|
91
|
+
cancel_current_signing if @current_signing_request
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def handle_sign_request(message, sender, send_response)
|
|
95
|
+
payload = message[:payload] || `message.payload`
|
|
96
|
+
|
|
97
|
+
# Validate origin
|
|
98
|
+
origin = `sender.origin || sender.url`
|
|
99
|
+
unless origin_allowed?(origin)
|
|
100
|
+
send_error(send_response, message[:request_id],
|
|
101
|
+
Messaging::ErrorCodes::ORIGIN_NOT_ALLOWED,
|
|
102
|
+
"Origin not allowed: #{origin}")
|
|
103
|
+
return
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Store the request for completion after PIN entry
|
|
107
|
+
@current_signing_request = {
|
|
108
|
+
message: message,
|
|
109
|
+
sender: sender,
|
|
110
|
+
send_response: send_response,
|
|
111
|
+
payload: payload
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
# Open popup for PIN entry
|
|
115
|
+
open_pin_popup
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def handle_verify_request(message, sender, send_response)
|
|
119
|
+
payload = message[:payload] || `message.payload`
|
|
120
|
+
|
|
121
|
+
# Verification doesn't need PIN, send directly to native host
|
|
122
|
+
ensure_native_connection do |error|
|
|
123
|
+
if error
|
|
124
|
+
send_error(send_response, message[:request_id],
|
|
125
|
+
Messaging::ErrorCodes::NATIVE_HOST_NOT_FOUND, error)
|
|
126
|
+
return
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
native_message = {
|
|
130
|
+
type: "verify",
|
|
131
|
+
requestId: message[:request_id] || `message.requestId`,
|
|
132
|
+
payload: {
|
|
133
|
+
pdfData: payload[:pdf_data] || `payload.pdfData`,
|
|
134
|
+
checkTimestamp: payload[:check_timestamp] != false
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
send_to_native(native_message, send_response)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def handle_availability_request(send_response)
|
|
143
|
+
ensure_native_connection do |error|
|
|
144
|
+
if error
|
|
145
|
+
# Native host not available, but extension is
|
|
146
|
+
response = Messaging.create_response(
|
|
147
|
+
nil,
|
|
148
|
+
Messaging::Types::AVAILABILITY_RESPONSE,
|
|
149
|
+
{ available: false, nativeHostInstalled: false, error: error }
|
|
150
|
+
)
|
|
151
|
+
`sendResponse(#{response.to_n})`
|
|
152
|
+
return
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
native_message = { type: "check_availability", requestId: Messaging.generate_request_id }
|
|
156
|
+
send_to_native(native_message, send_response)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def handle_cancel_request(request_id, send_response)
|
|
161
|
+
if @current_signing_request && @current_signing_request[:message][:request_id] == request_id
|
|
162
|
+
cancel_current_signing
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
response = Messaging.create_response(request_id, "cancel_response", { cancelled: true })
|
|
166
|
+
`sendResponse(#{response.to_n})`
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def complete_signing_with_pin(pin)
|
|
170
|
+
return unless @current_signing_request
|
|
171
|
+
|
|
172
|
+
request = @current_signing_request
|
|
173
|
+
message = request[:message]
|
|
174
|
+
payload = request[:payload]
|
|
175
|
+
send_response = request[:send_response]
|
|
176
|
+
|
|
177
|
+
ensure_native_connection do |error|
|
|
178
|
+
if error
|
|
179
|
+
send_error(send_response, message[:request_id],
|
|
180
|
+
Messaging::ErrorCodes::NATIVE_HOST_NOT_FOUND, error)
|
|
181
|
+
@current_signing_request = nil
|
|
182
|
+
return
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
native_message = {
|
|
186
|
+
type: "sign",
|
|
187
|
+
requestId: message[:request_id] || `message.requestId`,
|
|
188
|
+
payload: {
|
|
189
|
+
pdfData: payload[:pdf_data] || `payload.pdfData`,
|
|
190
|
+
pin: pin,
|
|
191
|
+
options: payload[:options] || `payload.options` || {}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
send_to_native(native_message, send_response)
|
|
196
|
+
@current_signing_request = nil
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def cancel_current_signing
|
|
201
|
+
return unless @current_signing_request
|
|
202
|
+
|
|
203
|
+
request = @current_signing_request
|
|
204
|
+
send_response = request[:send_response]
|
|
205
|
+
request_id = request[:message][:request_id]
|
|
206
|
+
|
|
207
|
+
send_error(send_response, request_id, Messaging::ErrorCodes::CANCELLED, "Operation cancelled by user")
|
|
208
|
+
@current_signing_request = nil
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def send_signing_context_to_popup
|
|
212
|
+
return unless @popup_port && @current_signing_request
|
|
213
|
+
|
|
214
|
+
context = {
|
|
215
|
+
type: "signing_context",
|
|
216
|
+
origin: @current_signing_request[:sender] && `#{@current_signing_request[:sender]}.origin`,
|
|
217
|
+
options: @current_signing_request[:payload][:options] || {}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
`#{@popup_port}.postMessage(#{context.to_n})`
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def open_pin_popup
|
|
224
|
+
# Open extension popup programmatically
|
|
225
|
+
`chrome.action.openPopup().catch((e) => {
|
|
226
|
+
console.error('Failed to open popup:', e);
|
|
227
|
+
// Fallback: create a new window
|
|
228
|
+
chrome.windows.create({
|
|
229
|
+
url: 'popup/popup.html',
|
|
230
|
+
type: 'popup',
|
|
231
|
+
width: 400,
|
|
232
|
+
height: 300
|
|
233
|
+
});
|
|
234
|
+
})`
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def ensure_native_connection(&block)
|
|
238
|
+
if @native_port
|
|
239
|
+
block.call(nil)
|
|
240
|
+
return
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
begin
|
|
244
|
+
@native_port = `chrome.runtime.connectNative(#{NATIVE_HOST_NAME})`
|
|
245
|
+
|
|
246
|
+
`#{@native_port}.onMessage.addListener((msg) => {
|
|
247
|
+
#{handle_native_message(`msg`)}
|
|
248
|
+
})`
|
|
249
|
+
|
|
250
|
+
`#{@native_port}.onDisconnect.addListener(() => {
|
|
251
|
+
#{handle_native_disconnect}
|
|
252
|
+
})`
|
|
253
|
+
|
|
254
|
+
# Small delay to ensure connection is established
|
|
255
|
+
`setTimeout(() => { #{block.call(nil)} }, 50)`
|
|
256
|
+
rescue => e
|
|
257
|
+
block.call("Failed to connect to native host: #{e.message}")
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def send_to_native(message, send_response)
|
|
262
|
+
return unless @native_port
|
|
263
|
+
|
|
264
|
+
request_id = message[:requestId] || `message.requestId`
|
|
265
|
+
@pending_requests[request_id] = send_response
|
|
266
|
+
|
|
267
|
+
`#{@native_port}.postMessage(#{message.to_n})`
|
|
268
|
+
|
|
269
|
+
# Set timeout
|
|
270
|
+
`setTimeout(() => {
|
|
271
|
+
#{handle_request_timeout(request_id)}
|
|
272
|
+
}, #{Messaging::DEFAULT_TIMEOUT})`
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def handle_native_message(message)
|
|
276
|
+
request_id = message[:requestId] || `message.requestId` || message[:request_id]
|
|
277
|
+
send_response = @pending_requests.delete(request_id)
|
|
278
|
+
|
|
279
|
+
return unless send_response
|
|
280
|
+
|
|
281
|
+
# Forward response to content script
|
|
282
|
+
`sendResponse(#{message.to_n})`
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def handle_native_disconnect
|
|
286
|
+
error = `chrome.runtime.lastError?.message || 'Native host disconnected'`
|
|
287
|
+
|
|
288
|
+
@pending_requests.each do |request_id, send_response|
|
|
289
|
+
send_error(send_response, request_id, Messaging::ErrorCodes::NATIVE_HOST_NOT_FOUND, error)
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
@pending_requests.clear
|
|
293
|
+
@native_port = nil
|
|
294
|
+
|
|
295
|
+
# Cancel current signing request if any
|
|
296
|
+
cancel_current_signing
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def handle_request_timeout(request_id)
|
|
300
|
+
send_response = @pending_requests.delete(request_id)
|
|
301
|
+
return unless send_response
|
|
302
|
+
|
|
303
|
+
send_error(send_response, request_id, Messaging::ErrorCodes::TIMEOUT, "Operation timed out")
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def send_error(send_response, request_id, code, message)
|
|
307
|
+
response = Messaging.create_error(request_id, code, message)
|
|
308
|
+
`sendResponse(#{response.to_n})`
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def origin_allowed?(origin)
|
|
312
|
+
# TODO: Make this configurable via extension options
|
|
313
|
+
# For now, allow localhost and https origins
|
|
314
|
+
return true if origin =~ /^https?:\/\/localhost/
|
|
315
|
+
return true if origin =~ /^https:\/\//
|
|
316
|
+
|
|
317
|
+
false
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
# Initialize background service worker
|
|
323
|
+
EasySign::Background.new
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# backtick_javascript: true
|
|
3
|
+
|
|
4
|
+
require "native"
|
|
5
|
+
require "easy_sign/messaging"
|
|
6
|
+
|
|
7
|
+
# Content script for EasySign browser extension
|
|
8
|
+
# Bridges communication between web page and extension background
|
|
9
|
+
module EasySign
|
|
10
|
+
class ContentScript
|
|
11
|
+
def initialize
|
|
12
|
+
@pending_responses = {}
|
|
13
|
+
setup_page_listener
|
|
14
|
+
inject_api_script
|
|
15
|
+
puts "EasySign content script initialized"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def setup_page_listener
|
|
19
|
+
# Listen for messages from the page (inject.js)
|
|
20
|
+
`window.addEventListener('message', (event) => {
|
|
21
|
+
// Only accept messages from the same window
|
|
22
|
+
if (event.source !== window) return;
|
|
23
|
+
|
|
24
|
+
// Only accept messages targeted at the extension
|
|
25
|
+
if (!event.data || event.data.target !== #{Messaging::Targets::EXTENSION.to_n}) return;
|
|
26
|
+
|
|
27
|
+
#{handle_page_message(`event.data`, `event.origin`)}
|
|
28
|
+
})`
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def handle_page_message(data, origin)
|
|
32
|
+
request_id = data[:request_id] || `data.requestId` || `data.request_id`
|
|
33
|
+
type = data[:type] || `data.type`
|
|
34
|
+
|
|
35
|
+
# Forward to background script
|
|
36
|
+
`chrome.runtime.sendMessage(#{data.to_n}, (response) => {
|
|
37
|
+
#{handle_background_response(`response`, request_id)}
|
|
38
|
+
})`
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def handle_background_response(response, request_id)
|
|
42
|
+
# Check for chrome runtime errors
|
|
43
|
+
error = `chrome.runtime.lastError`
|
|
44
|
+
if error
|
|
45
|
+
response = Messaging.create_error(
|
|
46
|
+
request_id,
|
|
47
|
+
Messaging::ErrorCodes::INTERNAL_ERROR,
|
|
48
|
+
`error.message || 'Extension error'`
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Send response back to page
|
|
53
|
+
post_to_page(response)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def post_to_page(message)
|
|
57
|
+
page_message = (message || {}).merge(target: Messaging::Targets::PAGE)
|
|
58
|
+
`window.postMessage(#{page_message.to_n}, '*')`
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def inject_api_script
|
|
62
|
+
# Inject the page-context script that exposes window.EasySign
|
|
63
|
+
`const script = document.createElement('script');
|
|
64
|
+
script.src = chrome.runtime.getURL('inject.js');
|
|
65
|
+
script.onload = function() {
|
|
66
|
+
this.remove();
|
|
67
|
+
};
|
|
68
|
+
(document.head || document.documentElement).appendChild(script);`
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Initialize content script
|
|
74
|
+
EasySign::ContentScript.new
|