///|
/// Full-block CFB encryption (NIST SP 800-38A CFB128) with a
/// caller-provided IV. The final block may be partial; only the needed
/// keystream octets are consumed.
pub fn cfb_encrypt(
  bc : &@primitives.BlockCipher,
  iv : BytesView,
  data : BytesView,
) -> Bytes raise {
  let bs = bc.block_size()
  guard iv.length() == bs else { @error.fail("cfb: bad IV length") }
  let mut fr = iv.to_owned()
  let out = Buffer(size_hint=data.length())
  let mut off = 0
  while off < data.length() {
    let keystream = bc.encrypt_block(fr.exact_view())
    let end = if off + bs < data.length() { off + bs } else { data.length() }
    let ct = @primitives.xor_bytes(
      data[off:end],
      keystream.exact_view()[0:end - off],
    )
    out.write_bytes(ct.exact_view())
    if end - off == bs {
      fr = ct
    }
    off = end
  }
  out.to_bytes()
}

///|
/// Full-block CFB decryption with a caller-provided IV. The final block
/// may be partial.
pub fn cfb_decrypt(
  bc : &@primitives.BlockCipher,
  iv : BytesView,
  data : BytesView,
) -> Bytes raise {
  let bs = bc.block_size()
  guard iv.length() == bs else { @error.fail("cfb: bad IV length") }
  let mut fr = iv.to_owned()
  let out = Buffer(size_hint=data.length())
  let mut off = 0
  while off < data.length() {
    let keystream = bc.encrypt_block(fr.exact_view())
    let end = if off + bs < data.length() { off + bs } else { data.length() }
    let pt = @primitives.xor_bytes(
      data[off:end],
      keystream.exact_view()[0:end - off],
    )
    out.write_bytes(pt.exact_view())
    if end - off == bs {
      fr = data[off:end].to_owned()
    }
    off = end
  }
  out.to_bytes()
}

///|
/// The random prefix of SEIPD v1 / OpenPGP CFB: `bs` random octets then a
/// copy of the last two (RFC 9580 §5.13.1). `bs` is the block size of the
/// session-key cipher — 16 for AES but 8 for the legacy CAST5/3DES, where
/// the old hardcoded 16 quietly produced an invalid prefix.
pub fn random_prefix(bs : Int32) -> Bytes raise {
  let p = @primitives.random_bytes(bs)
  p.add(p[bs - 2:bs].to_owned())
}

///|
/// Encrypt with the legacy OpenPGP CFB variant used by the obsolete
/// Symmetrically Encrypted Data packet (tag 9): the 18-octet prefix is
/// encrypted in one CFB context with an all-zero IV, then the CFB state
/// is resynchronized with the last block-size octets of ciphertext
/// (RFC 9580 §5.7).
pub fn openpgp_cfb_encrypt_resync(
  bc : &@primitives.BlockCipher,
  prefix : BytesView,
  data : BytesView,
) -> Bytes raise {
  let bs = bc.block_size()
  guard prefix.length() == bs + 2 else { @error.fail("cfb: bad prefix length") }
  let zeros = Bytes::make(bs, b'\x00')
  // First block: C0 = P0 XOR E(0).
  let ks0 = bc.encrypt_block(zeros.exact_view())
  let c0 = @primitives.xor_bytes(prefix[0:bs], ks0.exact_view())
  // Check bytes: C[bs..bs+2] = P[bs..bs+2] XOR E(C0)[0..2].
  let ks1 = bc.encrypt_block(c0.exact_view())
  let checks = @primitives.xor_bytes(prefix[bs:bs + 2], ks1.exact_view()[0:2])
  let head = c0.add(checks)
  // Resynchronize: FR = C[2..bs+2].
  let fr = head.exact_view()[2:bs + 2]
  let rest = cfb_encrypt(bc, fr, data)
  head.add(rest)
}

///|
/// Decrypt the resynchronized OpenPGP CFB variant. Verifies the two check
/// octets of the prefix and returns the data (prefix stripped).
pub fn openpgp_cfb_decrypt_resync(
  bc : &@primitives.BlockCipher,
  ciphertext : BytesView,
) -> Bytes raise {
  let bs = bc.block_size()
  guard ciphertext.length() >= bs + 2 else {
    @error.fail("cfb: ciphertext too short")
  }
  let zeros = Bytes::make(bs, b'\x00')
  let ks0 = bc.encrypt_block(zeros.exact_view())
  let p0 = @primitives.xor_bytes(
    ciphertext.exact_view()[0:bs],
    ks0.exact_view(),
  )
  // Check bytes come from E(C0) keystream.
  let ks1 = bc.encrypt_block(ciphertext.exact_view()[0:bs])
  let checks = @primitives.xor_bytes(
    ciphertext.exact_view()[bs:bs + 2],
    ks1.exact_view()[0:2],
  )
  let prefix = p0.add(checks)
  guard prefix[bs - 2] == prefix[bs] && prefix[bs - 1] == prefix[bs + 1] else {
    @error.fail("cipher: CFB quick check failed (wrong session key?)")
  }
  // Resynchronize: FR = C[2..bs+2].
  let fr = ciphertext.exact_view()[2:bs + 2]
  cfb_decrypt(bc, fr, ciphertext.exact_view()[bs + 2:ciphertext.length()])
}