Class: Ksef::Crypto::Encryptor

Inherits:
Object
  • Object
show all
Defined in:
lib/ksef/crypto/encryptor.rb

Overview

A session's symmetric key, and the AES-256-CBC encryption done under it (docs/REFERENCE.md §10.1).

The IV is not prefixed to the ciphertext

This is the trap worth knowing about before reading anything else here. sesja-interaktywna.md describes the IV as "dołączanego jako prefiks do szyfrogramu" — prefixed to the ciphertext. It is not, and following that prose produces a payload KSeF cannot decrypt. The IV travels once, as a discrete field of the session-open request, and every per-invoice ciphertext is bare. Three higher-precedence sources agree against the prose, the OpenAPI contract among them (§14.1) — and the contract's own worked example settles it arithmetically: a 6480-byte invoice becomes 6496 bytes encrypted. 6480 is a whole number of blocks, so PKCS#7 adds exactly one block of padding; a prefixed IV would have made it 6512.

So #encrypt returns bare ciphertext, and the IV is exposed separately for the session-open request via #encryption_info.

Defined Under Namespace

Classes: Sealed

Constant Summary collapse

REDACTED =
"[REDACTED]"

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(key:, iv:) ⇒ Encryptor

Returns a new instance of Encryptor.

Parameters:

  • key (String) —

    32 raw bytes

  • iv (String) —

    16 raw bytes

Raises:



48
49
50
51
52
# File 'lib/ksef/crypto/encryptor.rb', line 48

def initialize(key:, iv:)
  @key = check("key", key, KEY_BYTES)
  @iv = check("iv", iv, IV_BYTES)
  freeze
end

Instance Attribute Details

#iv ⇒ Object (readonly)

The IV is not secret — it is sent to the API in the clear — so unlike the key it has a reader. It is still kept out of #inspect, which DESIGN.md §4.5 requires.



36
37
38
# File 'lib/ksef/crypto/encryptor.rb', line 36

def iv
  @iv
end

Class Method Details

.generate ⇒ Object

A fresh key and IV from the CSPRNG. §10.1 records a per-session key as recommended by the docs rather than required; recommended is reason enough.



40
41
42
43
# File 'lib/ksef/crypto/encryptor.rb', line 40

def self.generate
  cipher = OpenSSL::Cipher.new(CIPHER)
  new(key: cipher.random_key, iv: cipher.random_iv)
end

Instance Method Details

#decrypt(ciphertext) ⇒ Object

The inverse. KSeF never asks us to decrypt anything, so this exists for tests and for a caller wanting to prove to itself that a payload round-trips before sending it — which beats discovering a key mismatch from a rejected invoice.



63
# File 'lib/ksef/crypto/encryptor.rb', line 63

def decrypt(ciphertext) = transform(:decrypt, ciphertext)

#encrypt(plaintext) ⇒ String

Returns bare ciphertext, binary — see the class note on the IV.

Parameters:

  • plaintext (String) —

    the invoice XML, already serialised. Deliberately not #to_xml-coercing: what gets hashed has to be exactly what gets encrypted, so the bytes are settled one layer up.

Returns:

  • (String) —

    bare ciphertext, binary — see the class note on the IV



58
# File 'lib/ksef/crypto/encryptor.rb', line 58

def encrypt(plaintext) = transform(:encrypt, plaintext)

#encryption_info(certificate) ⇒ Hash

The contract's EncryptionInfo, as sent on POST /sessions/online, POST /sessions/batch and POST /invoices/exports.

publicKeyId is nullable in the contract but always sent here: it names which published key did the wrapping, and without it a key rotation turns a decryptable payload into an undecryptable one with nothing to diagnose it by (§10.2).

Parameters:

Returns:

  • (Hash)


87
88
89
90
91
92
93
# File 'lib/ksef/crypto/encryptor.rb', line 87

def encryption_info(certificate)
  {
    encryptedSymmetricKey: Crypto.encode(certificate.encrypt(@key)),
    initializationVector: Crypto.encode(@iv),
    publicKeyId: certificate.public_key_id
  }
end

#inspect ⇒ Object



99
# File 'lib/ksef/crypto/encryptor.rb', line 99

def inspect = "#<Ksef::Crypto::Encryptor key=#{REDACTED} iv=#{REDACTED}>"

#seal(plaintext) ⇒ Sealed

Encrypt once, and measure both artifacts while they are in hand.

Parameters:

  • plaintext (String)

Returns:



69
70
71
72
73
74
75
76
# File 'lib/ksef/crypto/encryptor.rb', line 69

def seal(plaintext)
  ciphertext = encrypt(plaintext)
  Sealed.new(
    ciphertext: ciphertext,
    plaintext_digest: Digest.of(plaintext),
    ciphertext_digest: Digest.of(ciphertext)
  )
end

#to_s ⇒ Object

Redacted, and #to_s deliberately too: DESIGN.md §4.5 forbids leaking symmetric keys and IVs at default log level, and "key: #{encryptor}" in someone's debug line is exactly how that happens.



98
# File 'lib/ksef/crypto/encryptor.rb', line 98

def to_s = REDACTED