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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 05f4bc4f464d21efa501041653ae1252c64f6d5cca1a29b30394b51ea0cdd486
4
- data.tar.gz: e26bba2fb9ccb37ecfaa5f32676453c9b5a5369b49d11f6cc2385a26345eead6
3
+ metadata.gz: 5370a4fedb6bf2bb2dc99ec4a8b28148f1caca417aca9f2401192c06a5fd49f9
4
+ data.tar.gz: fbe42537ec52fef71f1cd9806f2d91896c65e8e2ae4ad08aa8b9b864c52cf6aa
5
5
  SHA512:
6
- metadata.gz: 181d498ca94e2514ecf4c5296acf2b4cb5d975efbd77cfa0915280dcc610b7860740cb58ad1d3909f5ca137a30ca610347e10de6d85608c72fbe022e31923eb4
7
- data.tar.gz: d9ccd5c9bf41ba8f68415ea1ad9f72b68b48fa2799f76ce53726cd3fca93466a56e38fe6320b907c57cb67dbb7f8aac06674cd7cba7ac97a31ab44fb32d6ea11
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: "employee_badge",
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 |
@@ -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
@@ -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
@@ -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
@@ -2,5 +2,5 @@
2
2
 
3
3
  # lib/accessgrid/version.rb
4
4
  module AccessGrid
5
- VERSION = '0.4.0'
5
+ VERSION = '0.5.0'
6
6
  end
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.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-04-16 00:00:00.000000000 Z
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.4.19
63
+ rubygems_version: 3.5.9
63
64
  signing_key:
64
65
  specification_version: 4
65
66
  summary: AccessGrid API Client