accessgrid 0.4.0 → 0.5.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 +4 -4
- data/README.md +27 -1
- data/lib/accessgrid/console.rb +47 -0
- data/lib/accessgrid/error.rb +9 -0
- data/lib/accessgrid/request.rb +1 -1
- data/lib/accessgrid/smart_tap_reveal_crypto.rb +78 -0
- data/lib/accessgrid/version.rb +1 -1
- data/lib/accessgrid.rb +1 -0
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5370a4fedb6bf2bb2dc99ec4a8b28148f1caca417aca9f2401192c06a5fd49f9
|
|
4
|
+
data.tar.gz: fbe42537ec52fef71f1cd9806f2d91896c65e8e2ae4ad08aa8b9b864c52cf6aa
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c98e6d776bd2eede74712e6a6a08d0b87c67cc2161dc18748d84db28f34273b69aa6f8f2ce4d6bdb0a5f02c6a68a60cf1b043bed9a785402a373a3296e72521e
|
|
7
|
+
data.tar.gz: 6d8c089a3071b4064a5902823cfd8f88393fd0d60d80215a72860dd6e139d1a790b4a8a5b410a3311d54915b6a6c1fe94c7c3cc023d33c0a74691e2c886c1a1d
|
data/README.md
CHANGED
|
@@ -139,7 +139,7 @@ client.access_cards.delete("0xc4rd1d")
|
|
|
139
139
|
template = client.console.create_template(
|
|
140
140
|
name: "Employee Access Pass",
|
|
141
141
|
platform: "apple",
|
|
142
|
-
use_case: "
|
|
142
|
+
use_case: "corporate_id",
|
|
143
143
|
protocol: "desfire",
|
|
144
144
|
allow_on_multiple_devices: true,
|
|
145
145
|
watch_count: 2,
|
|
@@ -185,6 +185,30 @@ template = client.console.update_template(
|
|
|
185
185
|
template = client.console.read_template("0xd3adb00b5")
|
|
186
186
|
```
|
|
187
187
|
|
|
188
|
+
#### Publish a template
|
|
189
|
+
|
|
190
|
+
```ruby
|
|
191
|
+
result = client.console.publish_template("0xd3adb00b5")
|
|
192
|
+
|
|
193
|
+
puts result.id # "0xd3adb00b5"
|
|
194
|
+
puts result.status # "in-review" (Apple), "ready" (Android), or "publishing" (already in flight)
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
#### Reveal a SmartTap private key
|
|
198
|
+
|
|
199
|
+
Fetches the template's SmartTap private key, decrypted client-side. The SDK generates a fresh ephemeral P-256 keypair per call, submits the public half, and decrypts the server's response — you get the plaintext PEM back without touching any crypto.
|
|
200
|
+
|
|
201
|
+
```ruby
|
|
202
|
+
reveal = client.console.reveal_smart_tap("0xd3adb00b5")
|
|
203
|
+
|
|
204
|
+
puts "Key version: #{reveal.key_version}"
|
|
205
|
+
puts "Collector ID: #{reveal.collector_id}"
|
|
206
|
+
puts "Fingerprint: #{reveal.fingerprint}"
|
|
207
|
+
puts reveal.private_key # PEM — store in your reader/collector key vault
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
The server enforces single-use on pubkey fingerprint and rate-limits to 1 per minute per account. The SDK uses a fresh keypair every call, so single-use is satisfied automatically. Errors raised by the crypto path (`AccessGrid::DecryptError`, `AccessGrid::InvalidEnvelopeError`) and the HTTP path (`AccessGrid::AuthenticationError`, `AccessGrid::ResourceNotFoundError`, etc.) all descend from `AccessGrid::Error`.
|
|
211
|
+
|
|
188
212
|
#### Get event logs
|
|
189
213
|
|
|
190
214
|
```ruby
|
|
@@ -480,6 +504,8 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/access
|
|
|
480
504
|
| GET /v1/console/card-template-pairs | `console.list_pass_template_pairs()` | Y |
|
|
481
505
|
| POST /v1/console/card-template-pairs | `console.create_pass_template_pair()` | Y |
|
|
482
506
|
| POST /v1/console/card-templates/{id}/ios_preflight | `console.ios_preflight()` | Y |
|
|
507
|
+
| POST /v1/console/card-templates/{id}/publish | `console.publish_template()` | Y |
|
|
508
|
+
| POST /v1/console/card-templates/{id}/smart-tap/reveal | `console.reveal_smart_tap()` | Y |
|
|
483
509
|
| GET /v1/console/ledger-items | `console.list_ledger_items()` / `console.ledger_items()` | Y |
|
|
484
510
|
| GET /v1/console/webhooks | `console.webhooks.list()` | Y |
|
|
485
511
|
| POST /v1/console/webhooks | `console.webhooks.create()` | Y |
|
data/lib/accessgrid/console.rb
CHANGED
|
@@ -73,6 +73,29 @@ module AccessGrid
|
|
|
73
73
|
|
|
74
74
|
alias ledger_items list_ledger_items
|
|
75
75
|
|
|
76
|
+
def publish_template(template_id)
|
|
77
|
+
response = @client.make_request(:post, "/v1/console/card-templates/#{template_id}/publish")
|
|
78
|
+
PublishTemplateResponse.new(response)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Reveal the SmartTap private key for a card template, decrypted client-side.
|
|
82
|
+
#
|
|
83
|
+
# The SDK generates a fresh ephemeral P-256 keypair per call, submits the
|
|
84
|
+
# public half, and decrypts the server's response. The returned
|
|
85
|
+
# RevealTemplatePrivateKey carries the plaintext PEM in #private_key;
|
|
86
|
+
# the encrypted envelope is consumed internally and not exposed.
|
|
87
|
+
def reveal_smart_tap(template_id)
|
|
88
|
+
keypair = SmartTapRevealCrypto.generate_keypair
|
|
89
|
+
response = @client.make_request(
|
|
90
|
+
:post,
|
|
91
|
+
"/v1/console/card-templates/#{template_id}/smart-tap/reveal",
|
|
92
|
+
{ client_public_key: keypair[:pub_pem] }
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
plaintext = SmartTapRevealCrypto.decrypt_envelope(response['encrypted_private_key'], keypair[:priv])
|
|
96
|
+
RevealTemplatePrivateKey.new(response.merge('private_key' => plaintext))
|
|
97
|
+
end
|
|
98
|
+
|
|
76
99
|
def ios_preflight(card_template_id:, access_pass_ex_id:)
|
|
77
100
|
data = { access_pass_ex_id: access_pass_ex_id }
|
|
78
101
|
response = @client.make_request(:post, "/v1/console/card-templates/#{card_template_id}/ios_preflight", data)
|
|
@@ -188,6 +211,30 @@ module AccessGrid
|
|
|
188
211
|
end
|
|
189
212
|
end
|
|
190
213
|
|
|
214
|
+
# Result of publishing a card template.
|
|
215
|
+
class PublishTemplateResponse
|
|
216
|
+
attr_reader :id, :status
|
|
217
|
+
|
|
218
|
+
def initialize(data)
|
|
219
|
+
@id = data['id']
|
|
220
|
+
@status = data['status']
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Result of revealing a SmartTap private key. #private_key is the plaintext
|
|
225
|
+
# PEM, decrypted client-side by the SDK; the encrypted envelope is consumed
|
|
226
|
+
# internally and not exposed.
|
|
227
|
+
class RevealTemplatePrivateKey
|
|
228
|
+
attr_reader :key_version, :collector_id, :fingerprint, :private_key
|
|
229
|
+
|
|
230
|
+
def initialize(data)
|
|
231
|
+
@key_version = data['key_version']
|
|
232
|
+
@collector_id = data['collector_id']
|
|
233
|
+
@fingerprint = data['fingerprint']
|
|
234
|
+
@private_key = data['private_key']
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
191
238
|
# Represents a billing ledger item.
|
|
192
239
|
class LedgerItem
|
|
193
240
|
attr_reader :created_at, :amount, :id, :kind, :metadata, :access_pass
|
data/lib/accessgrid/error.rb
CHANGED
|
@@ -13,6 +13,15 @@ module AccessGrid
|
|
|
13
13
|
# Raised when request parameters fail validation.
|
|
14
14
|
class ValidationError < Error; end
|
|
15
15
|
|
|
16
|
+
# Raised when a SmartTap reveal envelope is missing fields or contains
|
|
17
|
+
# non-base64 / non-PEM data.
|
|
18
|
+
class InvalidEnvelopeError < Error; end
|
|
19
|
+
|
|
20
|
+
# Raised when AES-GCM auth-tag verification fails while decrypting a
|
|
21
|
+
# SmartTap reveal envelope (wrong key, tampered envelope, or wire-format
|
|
22
|
+
# drift between server and SDK).
|
|
23
|
+
class DecryptError < Error; end
|
|
24
|
+
|
|
16
25
|
# additional error classes to match Python version
|
|
17
26
|
class AccessGridError < Error; end
|
|
18
27
|
end
|
data/lib/accessgrid/request.rb
CHANGED
|
@@ -82,7 +82,7 @@ module AccessGrid
|
|
|
82
82
|
|
|
83
83
|
last_part = parts.last
|
|
84
84
|
second_to_last_part = parts[-2]
|
|
85
|
-
@resource_id = %w[suspend resume unlink delete].include?(last_part) ? second_to_last_part : last_part
|
|
85
|
+
@resource_id = %w[suspend resume unlink delete publish].include?(last_part) ? second_to_last_part : last_part
|
|
86
86
|
end
|
|
87
87
|
|
|
88
88
|
def get?
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'openssl'
|
|
4
|
+
require 'base64'
|
|
5
|
+
|
|
6
|
+
module AccessGrid
|
|
7
|
+
# Internal crypto helpers for the SmartTap reveal flow.
|
|
8
|
+
#
|
|
9
|
+
# Driven by Console#reveal_smart_tap; not part of the public SDK surface.
|
|
10
|
+
# Pure stdlib — no new gem deps.
|
|
11
|
+
#
|
|
12
|
+
# @api private
|
|
13
|
+
module SmartTapRevealCrypto
|
|
14
|
+
CURVE = 'prime256v1'
|
|
15
|
+
HKDF_INFO = 'accessgrid-smart-tap-reveal-v1'
|
|
16
|
+
KEY_LEN = 32
|
|
17
|
+
|
|
18
|
+
# Generate a fresh ephemeral P-256 keypair for a reveal call.
|
|
19
|
+
#
|
|
20
|
+
# @return [Hash] `{priv: OpenSSL::PKey::EC, pub_pem: String}`
|
|
21
|
+
def self.generate_keypair
|
|
22
|
+
priv = OpenSSL::PKey::EC.generate(CURVE)
|
|
23
|
+
{ priv: priv, pub_pem: priv.public_to_pem }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Decrypt the encrypted_private_key envelope from the reveal endpoint.
|
|
27
|
+
#
|
|
28
|
+
# Performs ECDH(client_priv, server_ephemeral_pub) + HKDF-SHA256 +
|
|
29
|
+
# AES-256-GCM. Must match the server-side encryption parameters exactly.
|
|
30
|
+
#
|
|
31
|
+
# @return [String] the plaintext SmartTap private key PEM.
|
|
32
|
+
# @raise [RuntimeError] on missing/bad envelope or auth-tag verification failure.
|
|
33
|
+
def self.decrypt_envelope(envelope, priv)
|
|
34
|
+
server_pub = parse_ephemeral_public_key(envelope)
|
|
35
|
+
nonce = decode_envelope_bytes(envelope['iv'])
|
|
36
|
+
ciphertext = decode_envelope_bytes(envelope['ciphertext'])
|
|
37
|
+
tag = decode_envelope_bytes(envelope['tag'])
|
|
38
|
+
|
|
39
|
+
aes_key = derive_aes_key(priv, server_pub)
|
|
40
|
+
aes_gcm_decrypt(aes_key, nonce, ciphertext, tag)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# @api private
|
|
44
|
+
def self.parse_ephemeral_public_key(envelope)
|
|
45
|
+
pem = envelope['ephemeral_public_key']
|
|
46
|
+
raise InvalidEnvelopeError, 'Invalid ephemeral_public_key in envelope' unless pem.is_a?(String) && !pem.empty?
|
|
47
|
+
|
|
48
|
+
OpenSSL::PKey::EC.new(pem)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# @api private
|
|
52
|
+
def self.derive_aes_key(priv, server_pub)
|
|
53
|
+
shared_secret = priv.dh_compute_key(server_pub.public_key)
|
|
54
|
+
OpenSSL::KDF.hkdf(shared_secret, salt: '', info: HKDF_INFO, length: KEY_LEN, hash: 'SHA256')
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# @api private
|
|
58
|
+
def self.aes_gcm_decrypt(aes_key, nonce, ciphertext, tag)
|
|
59
|
+
cipher = OpenSSL::Cipher.new('aes-256-gcm').decrypt
|
|
60
|
+
cipher.key = aes_key
|
|
61
|
+
cipher.iv = nonce
|
|
62
|
+
cipher.auth_tag = tag
|
|
63
|
+
cipher.auth_data = ''
|
|
64
|
+
cipher.update(ciphertext) + cipher.final
|
|
65
|
+
rescue OpenSSL::Cipher::CipherError
|
|
66
|
+
raise DecryptError, 'AES-GCM decryption failed (auth tag verification)'
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# @api private
|
|
70
|
+
def self.decode_envelope_bytes(value)
|
|
71
|
+
raise InvalidEnvelopeError, 'Envelope iv/ciphertext/tag must be base64-encoded' unless value.is_a?(String)
|
|
72
|
+
|
|
73
|
+
Base64.strict_decode64(value)
|
|
74
|
+
rescue ArgumentError
|
|
75
|
+
raise InvalidEnvelopeError, 'Envelope iv/ciphertext/tag must be base64-encoded'
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
data/lib/accessgrid/version.rb
CHANGED
data/lib/accessgrid.rb
CHANGED
|
@@ -11,6 +11,7 @@ require_relative 'accessgrid/access_cards'
|
|
|
11
11
|
require_relative 'accessgrid/console'
|
|
12
12
|
require_relative 'accessgrid/error'
|
|
13
13
|
require_relative 'accessgrid/request'
|
|
14
|
+
require_relative 'accessgrid/smart_tap_reveal_crypto'
|
|
14
15
|
require_relative 'accessgrid/version'
|
|
15
16
|
|
|
16
17
|
# Ruby SDK for the AccessGrid API.
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: accessgrid
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Auston Bunsen
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-05-27 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: base64
|
|
@@ -38,6 +38,7 @@ files:
|
|
|
38
38
|
- lib/accessgrid/console.rb
|
|
39
39
|
- lib/accessgrid/error.rb
|
|
40
40
|
- lib/accessgrid/request.rb
|
|
41
|
+
- lib/accessgrid/smart_tap_reveal_crypto.rb
|
|
41
42
|
- lib/accessgrid/version.rb
|
|
42
43
|
homepage: https://github.com/access-grid/accessgrid-rb
|
|
43
44
|
licenses:
|
|
@@ -59,7 +60,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
59
60
|
- !ruby/object:Gem::Version
|
|
60
61
|
version: '0'
|
|
61
62
|
requirements: []
|
|
62
|
-
rubygems_version: 3.
|
|
63
|
+
rubygems_version: 3.5.9
|
|
63
64
|
signing_key:
|
|
64
65
|
specification_version: 4
|
|
65
66
|
summary: AccessGrid API Client
|