///|
fn already_decrypted(pdf : @pdf.Pdf, objnum : Int) -> Bool {
  try {
    let entry = @pdf.pdfobjmap_find(objnum, pdf.objects.objects)
    match entry.0.val {
      @pdf.ObjectData::ParsedAlreadyDecrypted(_) => true
      _ => false
    }
  } catch {
    _ => false
  }
}

///|
fn is_identity(
  no_encrypt_metadata : Bool,
  pdf : @pdf.Pdf,
  dict : @pdf.PdfObject,
) -> Bool {
  let identity_crypt_filter_present = match pdf.lookup_direct("/Filter", dict) {
    Some(Name(name)) if name.equal_string_bytes("/Crypt") => true
    Some(Array([Name(name), ..])) if name.equal_string_bytes("/Crypt") => true
    _ => false
  }
  let identity_param = if identity_crypt_filter_present {
    match pdf.lookup_direct("/DecodeParms", dict) {
      Some(Dictionary(decodeparms)) =>
        match
          pdf.lookup_direct("/Name", @pdf.PdfObject::Dictionary(decodeparms)) {
          Some(Name(name)) if name.equal_string_bytes("/Identity") => true
          None => true
          _ => false
        }
      Some(Array([Dictionary(decodeparms), ..])) =>
        match
          pdf.lookup_direct("/Name", @pdf.PdfObject::Dictionary(decodeparms)) {
          Some(Name(name)) if name.equal_string_bytes("/Identity") => true
          None => true
          _ => false
        }
      Some(Array(_)) => true
      _ => true
    }
  } else {
    false
  }
  let is_metadata = if no_encrypt_metadata {
    match pdf.lookup_direct("/Type", dict) {
      Some(Name(name)) if name.equal_string_bytes("/Metadata") => true
      _ => false
    }
  } else {
    false
  }
  is_metadata || identity_param
}

///|
fn decrypt_stream(
  decrypt_obj : (@pdf.PdfObject) -> @pdf.PdfObject raise,
  crypt_type : @pdfcryptprimitives.Encryption,
  pdf : @pdf.Pdf,
  no_encrypt_metadata : Bool,
  encrypt : Bool,
  obj : Int,
  gen : Int,
  key : Array[Int],
  keylength : Int,
  r : Int,
  file_encryption_key : String?,
  stream : @pdf.PdfObject,
) -> @pdf.PdfObject raise {
  match (stream : @pdf.PdfObject) {
    Stream(rf) =>
      match rf.val {
        (dict, data) =>
          if is_identity(no_encrypt_metadata, pdf, dict) {
            stream
          } else {
            let prims = @pdfcryptprimitives.PdfCryptPrimitives::new()
            let crypt = @pdf.ToGetCrypt::ToDecrypt(@pdf.DeferredEncryption::{
              crypt_type,
              file_encryption_key,
              obj,
              gen,
              key,
              keylength,
              r,
            })
            let data_out : @pdf.Stream = match data {
              Got(bytes) => {
                if (r == 5 || r == 6) && file_encryption_key is None {
                  raise @pdf.PdfError::Msg("decrypt: no key C")
                }
                Got(
                  prims.decrypt_stream_data(
                    crypt_type, encrypt, file_encryption_key, obj, gen, key, keylength,
                    r, bytes,
                  ),
                )
              }
              ToGet(toget) =>
                if encrypt {
                  @pdf.PdfObject::Stream(rf).getstream()
                  match rf.val {
                    (_, Got(got_bytes)) => {
                      if (r == 5 || r == 6) && file_encryption_key is None {
                        raise @pdf.PdfError::Msg("decrypt: no key C")
                      }
                      Got(
                        prims.decrypt_stream_data(
                          crypt_type, encrypt, file_encryption_key, obj, gen, key,
                          keylength, r, got_bytes,
                        ),
                      )
                    }
                    _ => data
                  }
                } else {
                  ToGet(@pdf.ToGet::{
                    input: toget.input,
                    position: toget.position,
                    length: toget.length,
                    crypt,
                  })
                }
            }
            let dict_out : @pdf.PdfObject = match (dict : @pdf.PdfObject) {
              Dictionary(entries) => {
                let out = Array::new(capacity=entries.length())
                for entry in entries {
                  out.push((entry.0, decrypt_obj(entry.1)))
                }
                Dictionary(out)
              }
              _ => dict
            }
            let dict_updated = match data_out {
              Got(bytes) =>
                dict_out.replace_entry(
                  "/Length",
                  @pdf.PdfObject::Integer(bytes.length()),
                )
              _ => dict_out
            }
            rf.val = (dict_updated, data_out)
            Stream(rf)
          }
      }
    _ => stream
  }
}

///|
fn decrypt(
  crypt_type : @pdfcryptprimitives.Encryption,
  pdf : @pdf.Pdf,
  no_encrypt_metadata : Bool,
  encrypt : Bool,
  obj : Int,
  gen : Int,
  key : Array[Int],
  keylength : Int,
  r : Int,
  file_encryption_key : String?,
  objval : @pdf.PdfObject,
) -> @pdf.PdfObject raise {
  let decrypt_obj = fn(value : @pdf.PdfObject) -> @pdf.PdfObject raise {
    decrypt(
      crypt_type, pdf, no_encrypt_metadata, encrypt, obj, gen, key, keylength, r,
      file_encryption_key, value,
    )
  }
  match objval {
    String(s) =>
      if already_decrypted(pdf, obj) {
        objval
      } else {
        let s_bytes = @pdfio.bytes_of_string(s)
        if (r == 5 || r == 6) && file_encryption_key is None {
          raise @pdf.PdfError::Msg("decrypt: no key B")
        }
        let out_bytes = @pdfcryptprimitives.PdfCryptPrimitives::new().decrypt_stream_data(
          crypt_type, encrypt, file_encryption_key, obj, gen, key, keylength, r,
          s_bytes,
        )
        String(@pdfio.string_of_bytes(out_bytes))
      }
    Stream(_) =>
      decrypt_stream(
        decrypt_obj, crypt_type, pdf, no_encrypt_metadata, encrypt, obj, gen, key,
        keylength, r, file_encryption_key, objval,
      )
    Array(values) =>
      if already_decrypted(pdf, obj) {
        objval
      } else {
        let out = Array::new(capacity=values.length())
        for value in values {
          out.push(decrypt_obj(value))
        }
        Array(out)
      }
    Dictionary(values) =>
      if already_decrypted(pdf, obj) {
        objval
      } else {
        let out = Array::new(capacity=values.length())
        for entry in values {
          out.push((entry.0, decrypt_obj(entry.1)))
        }
        Dictionary(out)
      }
    _ => objval
  }
}

///|
fn process_cryption(
  no_encrypt_metadata : Bool,
  encrypt : Bool,
  pdf : @pdf.Pdf,
  crypt_type : @pdfcryptprimitives.Encryption,
  user_pw : String,
  r : Int,
  u : String,
  o : String,
  p : Int,
  id : String,
  keylength : Int,
  file_encryption_key : String?,
) -> @pdf.Pdf? raise {
  let mut encryption_object_number = -1
  match pdf.trailerdict.lookup_immediate("/Encrypt") {
    Some(Indirect(n)) => encryption_object_number = n
    _ => ()
  }
  let do_encryption = fn(key : Array[Int]) -> @pdf.Pdf? raise {
    let entries = pdf.objects.objects.to_array()
    entries.sort_by((a, b) => a.0 - b.0)
    for entry in entries {
      let objnum = entry.0
      let gennum = entry.1.1
      if objnum != encryption_object_number {
        let obj = pdf.lookup_obj(objnum)
        let new_obj = decrypt(
          crypt_type, pdf, no_encrypt_metadata, encrypt, objnum, gennum, key, keylength,
          r, file_encryption_key, obj,
        ) catch {
          _ => {
            @pdfe.log("Warning: unable to decrypt object \{objnum}\n")
            obj
          }
        }
        pdf.addobj_given_num((objnum, new_obj))
      }
    }
    pdf.trailerdict = pdf.trailerdict.remove_entry("/Encrypt")
    Some(pdf)
  }
  if r == 5 || r == 6 {
    match file_encryption_key {
      Some(k) => do_encryption(@pdfio.int_array_of_string(k))
      None => None
    }
  } else if authenticate_user(
      no_encrypt_metadata, user_pw, r, u, o, p, id, keylength,
    ) {
    do_encryption(
      find_key(no_encrypt_metadata, user_pw, r, o, p, id, keylength),
    )
  } else {
    None
  }
}

///|
/// Read encryption parameters from an encrypted PDF.
pub fn PdfCrypt::get_encryption_values(
  _self : PdfCrypt,
  pdf : @pdf.Pdf,
) -> (
  @pdfcryptprimitives.Encryption,
  String,
  String,
  Int,
  String,
  String?,
  String?,
) raise {
  let encryptdict = match pdf.lookup_direct("/Encrypt", pdf.trailerdict) {
    Some(value) => value
    None => raise @pdf.PdfError::Msg("get_encryption_values: unencrypted pdf")
  }
  let filter = pdf.lookup_direct("/Filter", encryptdict)
  let v_val = pdf.lookup_direct("/V", encryptdict)
  let length_val = pdf.lookup_direct("/Length", encryptdict)
  let r_val = pdf.lookup_direct("/R", encryptdict)
  let crypt_type = match (filter, v_val, length_val, r_val) {
    (
      Some(@pdf.PdfObject::Name(name)),
      Some(@pdf.PdfObject::Integer(1)),
      _,
      Some(@pdf.PdfObject::Integer(r)),
    ) if name.equal_string_bytes("/Standard") =>
      Some(@pdfcryptprimitives.Encryption::ARC4(40, r))
    (
      Some(@pdf.PdfObject::Name(name)),
      Some(@pdf.PdfObject::Integer(2)),
      None,
      Some(@pdf.PdfObject::Integer(r)),
    ) if name.equal_string_bytes("/Standard") =>
      Some(@pdfcryptprimitives.Encryption::ARC4(40, r))
    (
      Some(@pdf.PdfObject::Name(name)),
      Some(@pdf.PdfObject::Integer(2)),
      Some(@pdf.PdfObject::Integer(n)),
      _,
    ) if name.equal_string_bytes("/Standard") =>
      if n % 8 == 0 && n >= 40 && n <= 128 {
        Some(@pdfcryptprimitives.Encryption::ARC4(n, 3))
      } else {
        None
      }
    (
      Some(@pdf.PdfObject::Name(name)),
      Some(@pdf.PdfObject::Integer(v)),
      length,
      Some(@pdf.PdfObject::Integer(r)),
    ) if name.equal_string_bytes("/Standard") =>
      if v == 4 || v == 5 {
        match pdf.lookup_direct("/CF", encryptdict) {
          Some(cfdict) =>
            match pdf.lookup_direct("/StdCF", cfdict) {
              Some(stdcfdict) =>
                match pdf.lookup_direct("/CFM", stdcfdict) {
                  Some(@pdf.PdfObject::Name(cfm)) if cfm.equal_string_bytes(
                      "/V2",
                    ) =>
                    match length {
                      Some(@pdf.PdfObject::Integer(i)) =>
                        Some(@pdfcryptprimitives.Encryption::ARC4(i, 4))
                      _ =>
                        match pdf.lookup_direct("/Length", cfdict) {
                          Some(@pdf.PdfObject::Integer(i)) =>
                            Some(@pdfcryptprimitives.Encryption::ARC4(i, 4))
                          _ => None
                        }
                    }
                  Some(@pdf.PdfObject::Name(cfm)) if cfm.equal_string_bytes(
                      "/AESV2",
                    ) => Some(@pdfcryptprimitives.Encryption::AESV2)
                  Some(@pdf.PdfObject::Name(cfm)) if cfm.equal_string_bytes(
                      "/AESV3",
                    ) => Some(@pdfcryptprimitives.Encryption::AESV3(r == 6))
                  _ => None
                }
              _ => None
            }
          _ => None
        }
      } else {
        None
      }
    _ => None
  }
  let chop_string3248 = fn(name : String, s : String) -> String raise {
    let need = match crypt_type {
      Some(@pdfcryptprimitives.Encryption::AESV3(_)) => 48
      _ => 32
    }
    if s.length() < need {
      raise @pdf.PdfError::Msg("\{name}: too small in get_encryption_values")
    } else {
      s[0:need].to_string()
    }
  }
  match crypt_type {
    None => raise @pdf.PdfError::Msg("No encryption method")
    Some(ct) => {
      let o = match pdf.lookup_direct("/O", encryptdict) {
        Some(String(o)) => chop_string3248("/O", o)
        _ => raise @pdf.PdfError::Msg("Bad or missing /O entry")
      }
      let u = match pdf.lookup_direct("/U", encryptdict) {
        Some(String(u)) => chop_string3248("/U", u)
        _ => raise @pdf.PdfError::Msg("Bad or missing /U entry")
      }
      let p = match pdf.lookup_direct("/P", encryptdict) {
        Some(Integer(flags)) => flags
        _ => raise @pdf.PdfError::Msg("Bad or missing /P entry")
      }
      let id = match pdf.lookup_direct("/ID", pdf.trailerdict) {
        Some(Array([String(s), _])) => s
        _ => raise @pdf.PdfError::Msg("Bad or missing /ID element")
      }
      let oe = match pdf.lookup_direct("/OE", encryptdict) {
        Some(String(s)) => Some(s)
        _ => None
      }
      let ue = match pdf.lookup_direct("/UE", encryptdict) {
        Some(String(s)) => Some(s)
        _ => None
      }
      (ct, u, o, p, id, ue, oe)
    }
  }
}

///|
/// Decrypt a PDF document using the user password.
pub fn PdfCrypt::decrypt_pdf(
  self : PdfCrypt,
  user_pw : String,
  pdf : @pdf.Pdf,
  keyfromowner? : String,
) -> (@pdf.Pdf?, Array[Permission]) raise {
  match pdf.lookup_direct("/Encrypt", pdf.trailerdict) {
    None => (Some(pdf), [])
    Some(encrypt_dict) => {
      let (crypt_type, u, o, p, id, ue, oe) = self.get_encryption_values(pdf)
      let (r, keylength, file_encryption_key) = match
        (crypt_type : @pdfcryptprimitives.Encryption) {
        AESV2 => (4, 128, None)
        AESV3(iso) =>
          match (oe, ue) {
            (Some(_), Some(ue_val)) =>
              match keyfromowner {
                Some(k) => (5, 256, Some(k))
                None => {
                  let perms = match pdf.lookup_direct("/Perms", encrypt_dict) {
                    Some(String(s)) => s
                    _ =>
                      raise @pdf.PdfError::Msg(
                        "Missing /Perms in encryption dictionary",
                      )
                  }
                  let auth = authenticate_user_password_aesv3(
                    iso,
                    make_utf8(user_pw),
                    u,
                  )
                  if !auth {
                    (5, 256, None)
                  } else {
                    let key = file_encryption_key_aesv3_user(
                      iso,
                      make_utf8(user_pw),
                      u,
                      ue_val,
                    )
                    match p_of_perms(key, perms) {
                      None =>
                        raise Failure::Failure(
                          "/Perms file permissions corrupted",
                        )
                      Some(x) if x == p =>
                        (5, 256, Some(@pdfio.string_of_bytes(key)))
                      Some(_) =>
                        raise Failure::Failure(
                          "Mismatched /Perms and /P permissions",
                        )
                    }
                  }
                }
              }
            _ => raise Failure::Failure("decrypt_pdf: no oe")
          }
        ARC4(k, r_val) => (r_val, k, None)
      }
      let encrypt_metadata = match
        pdf.lookup_direct("/EncryptMetadata", encrypt_dict) {
        Some(Boolean(false)) => false
        _ => true
      }
      let perms = match pdf.lookup_direct("/Perms", encrypt_dict) {
        Some(String(s)) => s
        _ => ""
      }
      pdf.saved_encryption = Some(@pdf.SavedEncryption::{
        from_get_encryption_values: (crypt_type, u, o, p, id, ue, oe),
        encrypt_metadata,
        perms,
      })
      (
        process_cryption(
          !encrypt_metadata,
          false,
          pdf,
          crypt_type,
          user_pw,
          r,
          u,
          o,
          p,
          id,
          keylength,
          file_encryption_key,
        ),
        self.banlist_of_p(p),
      )
    }
  }
}

///|
fn key_or_user_password_from_owner(
  owner_pw : String,
  pdf : @pdf.Pdf,
  encryption_values? : (@pdfcryptprimitives.Encryption, String, String, String?),
) -> (String, String)? raise {
  let padded_owner = pad_password(@pdfio.int_array_of_string(owner_pw))
  let (crypt_type, u, o, oe) = match encryption_values {
    Some(values) => values
    None => {
      let (crypt_type, u, o, _, _, _, oe) = PdfCrypt::new().get_encryption_values(
        pdf,
      )
      (crypt_type, u, o, oe)
    }
  }
  let (r, keylength) = crypt_type.r_and_keylength()
  if r == 5 || r == 6 {
    if authenticate_owner_password_aesv3(r == 6, make_utf8(owner_pw), u, o) {
      match oe {
        Some(oe_val) => {
          let key = @pdfio.string_of_bytes(
            file_encryption_key_aesv3(r == 6, make_utf8(owner_pw), o, oe_val, u),
          )
          Some((key, ""))
        }
        None =>
          raise @pdf.PdfError::Msg("decrypt_pdf_owner: No /OE entry found")
      }
    } else {
      None
    }
  } else {
    let user_pw = {
      let prims = @pdfcryptprimitives.PdfCryptPrimitives::new()
      let key = owner_key(padded_owner, keylength, r)
      if r == 2 {
        @pdfio.string_of_bytes(prims.crypt(key, @pdfio.bytes_of_string(o)))
      } else {
        let mut acc = prims.crypt(key, @pdfio.bytes_of_string(o))
        for x in 19>=..0 {
          acc = prims.crypt(mkkey(key, x), acc)
        }
        @pdfio.string_of_bytes(acc)
      }
    }
    Some(("", user_pw))
  }
}

///|
/// Decrypt a PDF document using the owner password.
pub fn PdfCrypt::decrypt_pdf_owner(
  self : PdfCrypt,
  owner_pw : String,
  pdf : @pdf.Pdf,
) -> @pdf.Pdf? raise {
  match pdf.lookup_direct("/Encrypt", pdf.trailerdict) {
    None => Some(pdf)
    Some(_) =>
      match key_or_user_password_from_owner(owner_pw, pdf) {
        None => None
        Some((key, user_pw)) => {
          let (pdf_opt, _) = self.decrypt_pdf(user_pw, pdf, keyfromowner=key)
          pdf_opt
        }
      }
  }
}

///|
/// Just decrypt a single stream, given the user password and pdf.
pub fn PdfCrypt::decrypt_single_stream(
  self : PdfCrypt,
  user_pw : String?,
  owner_pw : String?,
  pdf : @pdf.Pdf,
  obj : Int,
  gen : Int,
  stream : @pdf.PdfObject,
) -> @pdf.PdfObject raise {
  match pdf.lookup_direct("/Encrypt", pdf.trailerdict) {
    None => stream
    Some(encrypt_dict) => {
      let (crypt_type, u, o, p, id, ue, oe) = self.get_encryption_values(pdf)
      let (r, keylength) = crypt_type.r_and_keylength()
      let no_encrypt_metadata = match
        pdf.lookup_direct("/EncryptMetadata", encrypt_dict) {
        Some(@pdf.PdfObject::Boolean(false)) => true
        _ => false
      }
      match (r, keylength, ue, oe) {
        (5 | 6, 256, Some(ue_val), Some(oe_val)) => {
          let owner_pw_val = match owner_pw {
            Some(value) => value
            None => ""
          }
          let user_pw_val = match user_pw {
            Some(value) => value
            None => ""
          }
          let file_encryption_key = if authenticate_user_password_aesv3(
              r == 6,
              make_utf8(user_pw_val),
              u,
            ) {
            file_encryption_key_aesv3_user(
              r == 6,
              make_utf8(user_pw_val),
              u,
              ue_val,
            )
          } else if authenticate_owner_password_aesv3(
              r == 6,
              make_utf8(owner_pw_val),
              u,
              o,
            ) {
            file_encryption_key_aesv3(
              r == 6,
              make_utf8(owner_pw_val),
              o,
              oe_val,
              u,
            )
          } else {
            raise @pdf.PdfError::Msg(
              "Encryption: Could not decrypt single stream: Bad or not supplied AESV3 user or owner password",
            )
          }
          decrypt_stream(
            value => decrypt(
              crypt_type,
              pdf,
              no_encrypt_metadata,
              false,
              obj,
              gen,
              @pdfio.int_array_of_bytes(file_encryption_key),
              keylength,
              r,
              Some(@pdfio.string_of_bytes(file_encryption_key)),
              value,
            ),
            crypt_type,
            pdf,
            no_encrypt_metadata,
            false,
            obj,
            gen,
            @pdfio.int_array_of_bytes(file_encryption_key),
            keylength,
            r,
            Some(@pdfio.string_of_bytes(file_encryption_key)),
            stream,
          )
        }
        _ =>
          match owner_pw {
            Some(owner_string) => {
              let prims = @pdfcryptprimitives.PdfCryptPrimitives::new()
              let padded_owner = pad_password(
                @pdfio.int_array_of_string(owner_string),
              )
              let key = owner_key(padded_owner, keylength, r)
              let user_pw_string = if r == 2 {
                @pdfio.string_of_bytes(
                  prims.crypt(key, @pdfio.bytes_of_string(o)),
                )
              } else {
                let mut acc = prims.crypt(key, @pdfio.bytes_of_string(o))
                for x in 19>=..0 {
                  acc = prims.crypt(mkkey(key, x), acc)
                }
                @pdfio.string_of_bytes(acc)
              }
              if authenticate_user(
                  no_encrypt_metadata, user_pw_string, r, u, o, p, id, keylength,
                ) {
                let user_key = find_key(
                  no_encrypt_metadata, user_pw_string, r, o, p, id, keylength,
                )
                decrypt_stream(
                  value => decrypt(
                    crypt_type,
                    pdf,
                    no_encrypt_metadata,
                    false,
                    obj,
                    gen,
                    user_key,
                    keylength,
                    r,
                    None,
                    value,
                  ),
                  crypt_type,
                  pdf,
                  no_encrypt_metadata,
                  false,
                  obj,
                  gen,
                  user_key,
                  keylength,
                  r,
                  None,
                  stream,
                )
              } else {
                raise @pdf.PdfError::Msg(
                  "Encryption: Bad or not supplied owner password when decrypting single stream",
                )
              }
            }
            None => {
              let user_pw_string = match user_pw {
                Some(value) => value
                None => ""
              }
              if authenticate_user(
                  no_encrypt_metadata, user_pw_string, r, u, o, p, id, keylength,
                ) {
                let user_key = find_key(
                  no_encrypt_metadata, user_pw_string, r, o, p, id, keylength,
                )
                decrypt_stream(
                  value => decrypt(
                    crypt_type,
                    pdf,
                    no_encrypt_metadata,
                    false,
                    obj,
                    gen,
                    user_key,
                    keylength,
                    r,
                    None,
                    value,
                  ),
                  crypt_type,
                  pdf,
                  no_encrypt_metadata,
                  false,
                  obj,
                  gen,
                  user_key,
                  keylength,
                  r,
                  None,
                  stream,
                )
              } else {
                raise @pdf.PdfError::Msg(
                  "Encryption: Bad or not supplied password when decrypting single stream",
                )
              }
            }
          }
      }
    }
  }
}

///|
fn load_saved_encryption_from_pdf(pdf : @pdf.Pdf) -> @pdf.SavedEncryption raise {
  let (crypt_type, u, o, p, id, ue, oe) = PdfCrypt::new().get_encryption_values(
    pdf,
  )
  let encrypt_dict = match pdf.lookup_direct("/Encrypt", pdf.trailerdict) {
    Some(value) => value
    None => raise @pdf.PdfError::Msg("recrypt_pdf: unencrypted pdf")
  }
  let encrypt_metadata = match
    pdf.lookup_direct("/EncryptMetadata", encrypt_dict) {
    Some(@pdf.PdfObject::Boolean(false)) => false
    _ => true
  }
  let perms = match pdf.lookup_direct("/Perms", encrypt_dict) {
    Some(@pdf.PdfObject::String(s)) => s
    _ => ""
  }
  {
    from_get_encryption_values: (crypt_type, u, o, p, id, ue, oe),
    encrypt_metadata,
    perms,
  }
}

///|
fn recrypt_pdf_user(pdf : @pdf.Pdf, pw : String) -> @pdf.Pdf raise {
  let saved = match pdf.saved_encryption {
    Some(value) => value
    None => {
      let saved = load_saved_encryption_from_pdf(pdf)
      pdf.saved_encryption = Some(saved)
      saved
    }
  }
  let (crypt_type, u, o, p, id, ue, oe) = saved.from_get_encryption_values
  let encrypt_metadata = saved.encrypt_metadata
  let perms = saved.perms
  match crypt_type {
    @pdfcryptprimitives.Encryption::AESV3(iso) => {
      let oe_val = match oe {
        Some(val) => val
        None => raise @pdf.PdfError::Msg("recrypt_pdf: bad /oe")
      }
      let ue_val = match ue {
        Some(val) => val
        None => raise @pdf.PdfError::Msg("recrypt_pdf: bad /ue")
      }
      let key = if authenticate_user_password_aesv3(iso, make_utf8(pw), u) {
        file_encryption_key_aesv3_user(iso, make_utf8(pw), u, ue_val)
      } else {
        raise @pdf.PdfError::Msg("recrypt_pdf: failed AESV3 fek.")
      }
      encrypt_pdf_AES256_inner(
        iso,
        encrypt_metadata,
        o,
        u,
        p,
        perms,
        oe_val,
        ue_val,
        id,
        @pdfio.string_of_bytes(key),
        pdf,
      )
    }
    @pdfcryptprimitives.Encryption::AESV2 =>
      encrypt_pdf_AES_inner(o, u, p, pw, id, encrypt_metadata, pdf)
    @pdfcryptprimitives.Encryption::ARC4(40, _) =>
      encrypt_pdf_40bit_inner(o, u, p, pw, id, pdf)
    @pdfcryptprimitives.Encryption::ARC4(128, 4) =>
      encrypt_pdf_128bit_inner_r4(o, u, p, pw, id, pdf, encrypt_metadata)
    @pdfcryptprimitives.Encryption::ARC4(128, _) =>
      encrypt_pdf_128bit_inner(o, u, p, pw, id, pdf)
    _ => raise @pdf.PdfError::Msg("recrypt_pdf: bad encryption")
  }
}

///|
fn recrypt_pdf_owner(pdf : @pdf.Pdf, owner_pw : String) -> @pdf.Pdf raise {
  let saved = match pdf.saved_encryption {
    Some(value) => value
    None => {
      let saved = load_saved_encryption_from_pdf(pdf)
      pdf.saved_encryption = Some(saved)
      saved
    }
  }
  let (crypt_type, u, o, p, id, ue, oe) = saved.from_get_encryption_values
  let encrypt_metadata = saved.encrypt_metadata
  let perms = saved.perms
  let key_and_pw = match
    key_or_user_password_from_owner(
      owner_pw,
      pdf,
      encryption_values=(crypt_type, u, o, oe),
    ) {
    None => raise @pdf.PdfError::Msg("Recrypt with owner password failed.")
    Some(pair) => pair
  }
  let key = key_and_pw.0
  let pw = key_and_pw.1
  match crypt_type {
    @pdfcryptprimitives.Encryption::AESV3(iso) => {
      let oe_val = match oe {
        Some(val) => val
        None => raise @pdf.PdfError::Msg("recrypt_pdf: bad /oe")
      }
      let ue_val = match ue {
        Some(val) => val
        None => raise @pdf.PdfError::Msg("recrypt_pdf: bad /ue")
      }
      encrypt_pdf_AES256_inner(
        iso, encrypt_metadata, o, u, p, perms, oe_val, ue_val, id, key, pdf,
      )
    }
    @pdfcryptprimitives.Encryption::AESV2 =>
      encrypt_pdf_AES_inner(o, u, p, pw, id, encrypt_metadata, pdf)
    @pdfcryptprimitives.Encryption::ARC4(40, _) =>
      encrypt_pdf_40bit_inner(o, u, p, pw, id, pdf)
    @pdfcryptprimitives.Encryption::ARC4(128, 4) =>
      encrypt_pdf_128bit_inner_r4(o, u, p, pw, id, pdf, encrypt_metadata)
    @pdfcryptprimitives.Encryption::ARC4(128, _) =>
      encrypt_pdf_128bit_inner(o, u, p, pw, id, pdf)
    _ => raise @pdf.PdfError::Msg("recrypt_pdf_owner: bad encryption")
  }
}

///|
/// Re-encrypt a PDF document, using the saved encryption metadata.
pub fn PdfCrypt::recrypt_pdf(
  self : PdfCrypt,
  pdf : @pdf.Pdf,
  pw : String,
  renumber? : Bool = true,
) -> @pdf.Pdf raise {
  let mut pdf = pdf
  if pdf.saved_encryption is None && self.is_encrypted(pdf) {
    let saved_result : Result[@pdf.SavedEncryption, Error] = try? load_saved_encryption_from_pdf(
      pdf,
    )
    match saved_result {
      Ok(saved) => pdf.saved_encryption = Some(saved)
      Err(_) => ()
    }
    let dec_result : Result[(@pdf.Pdf?, Array[Permission]), Error] = try? self.decrypt_pdf(
      pw,
      pdf.deep_copy(),
    )
    match dec_result {
      Ok((Some(pdf_dec), _)) => pdf = pdf_dec
      _ => {
        let owner_result : Result[@pdf.Pdf?, Error] = try? self.decrypt_pdf_owner(
          pw,
          pdf.deep_copy(),
        )
        match owner_result {
          Ok(Some(pdf_dec)) => pdf = pdf_dec
          _ => ()
        }
      }
    }
  }
  if renumber {
    let nums = pdf.objnumbers()
    let changes : Map[Int, Int] = Map::new(capacity=nums.length())
    let mut i = 1
    for n in nums {
      changes.set(n, i)
      i = i + 1
    }
    pdf = pdf.renumber(changes)
  }
  recrypt_pdf_user(pdf, pw) catch {
    _ =>
      recrypt_pdf_owner(pdf, pw) catch {
        _ => raise @pdf.PdfError::Msg("recrypt_pdf failed. Wrong password?")
      }
  }
}