All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.jlangch.venice.zipvault.venice Maven / Gradle / Ivy

;;;;   __    __         _
;;;;   \ \  / /__ _ __ (_) ___ ___
;;;;    \ \/ / _ \ '_ \| |/ __/ _ \
;;;;     \  /  __/ | | | | (_|  __/
;;;;      \/ \___|_| |_|_|\___\___|
;;;;
;;;;
;;;; Copyright 2017-2024 Venice
;;;;
;;;; Licensed under the Apache License, Version 2.0 (the "License");
;;;; you may not use this file except in compliance with the License.
;;;; You may obtain a copy of the License at
;;;;
;;;;     http://www.apache.org/licenses/LICENSE-2.0
;;;;
;;;; Unless required by applicable law or agreed to in writing, software
;;;; distributed under the License is distributed on an "AS IS" BASIS,
;;;; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
;;;; See the License for the specific language governing permissions and
;;;; limitations under the License.

;;;; ZIP Vault, AES-256 encrypted and password protected


(ns zipvault)

(import :org.repackage.net.lingala.zip4j.ZipFile)
(import :org.repackage.net.lingala.zip4j.io.outputstream.ZipOutputStream)
(import :org.repackage.net.lingala.zip4j.io.inputstream.ZipInputStream)
(import :org.repackage.net.lingala.zip4j.model.ZipParameters)
(import :org.repackage.net.lingala.zip4j.model.ExcludeFileFilter)
(import :org.repackage.net.lingala.zip4j.model.enums.EncryptionMethod)


(defn 
  ^{ :arglists '("(zipvault/zip out passphrase & entries)")
     :doc """
          Creates an AES-256 encrypted and password protected zip form the 
          entries and writes it to out. out may be a file or an output stream.

          An entry is given by a name and data. The entry data may be nil, a 
          bytebuf, a string, a file, an input stream, or a producer function. 
          An entry name with a trailing '/' creates a directory.

          Entry value types:

          | nil          | an empty file is written to the zip entry           |
          | bytebuf      | the bytes are written to the zip entry              |
          | string       | the string is written to the zip entry              |
          | file         | the content of the file is written to the zip entry |
          | input stream | the slurped input stream data is written to the \
                           zip entry |
          | function     | a producer function with a single output stream \
                           argument. All data written to the stream is written \
                           to the zip entry. The stream can be flushed but \
                           must not be closed! |

          **Passphrases:**

          The AES-256 algorithm requires a 256-bit key as input.
          One should use a passphrase with at least 128 bits of entropy 
          (that's roughly a 20-character passphrase of random 
          upper/lower/digits/symbols). 
          Less is dropping below general limits of safety, and more than 
          256 bits won't accomplish anything.

          See function: `zipvault/entropy`
          """
     :examples '(
          """
          (do 
            (load-module :zipvault)
            (zipvault/zip (io/file "vault.zip") "pwd")) ; empty zip
          """,
          """
          (do 
            (load-module :zipvault)
            (zipvault/zip (io/file "vault.zip") "pwd" "a.txt" "abc"))
          """,
          """
          (do 
            (load-module :zipvault)
            (zipvault/zip (io/file-out-stream "vault.zip")
                          "pwd"
                          "a.txt"       "abc"
                          "b.txt"       (bytebuf [100 101 102])))
          """,
          """
          (do 
            (load-module :zipvault)

            (let [file (io/file (io/tmp-dir) "c.txt")]
              (io/spit file "1234")
              (io/delete-file-on-exit c-tmp)

              ;; create "vault.zip"
              ;;          ├── a.txt
              ;;          ├── b.txt
              ;;          ├── c.txt
              ;;          ├── d.txt
              ;;          ├── e.txt
              ;;          ├── empty.txt
              ;;          └── xx
              ;;              └── g.txt
              (zipvault/zip 
                  (io/file "vault.zip")
                  "pwd"
                  "a.txt"       "abc"
                  "b.txt"       (bytebuf "def")
                  file          file  ; aquivalent:  (io/file-basename file) file
                  "d.txt"       (io/string-in-stream "ghi")
                  "e.txt"       (fn [os] 
                                  (let [wr (io/wrap-os-with-buffered-writer os)]
                                    (println wr "200")
                                    (flush wr)))
                  "empty.txt"   nil
                  "xx/g.txt"    "jkl")))
          """ )
     :see-also '(
          "zipvault/zip-folder"
          "zipvault/entries"
          "zipvault/add-files"
          "zipvault/add-folder"
          "zipvault/add-stream"
          "zipvault/remove-files"
          "zipvault/extract-file"
          "zipvault/extract-all"
          "zipvault/extract-file-data"
          "zipvault/entropy" ) }

  zip [out passphrase & entries]

  { :pre [(or (string? out) (io/file? out) (instance-of? :java.io.OutputStream out))
          (string? passphrase)] } 

  (let [params  (create-params)
        entries (apply hash-map entries)
        os      (if (file-or-string? out) 
                  (io/file-out-stream out :append false) 
                  out)]
    (zip-to-os params passphrase os entries)))



(defn 
  ^{ :arglists '(
      "(zipvault/zip-folder out passphrase folder)"
      "(zipvault/zip-folder out passphrase folder include-root-folder)"
      "(zipvault/zip-folder out passphrase folder include-root-folder exclude-fn)")
     :doc """
          Creates an AES-256 encrypted and password protected zip from the
          folder. 
          
          If 'include-root-folder' (default true) is true the root folder name 
          will be added to the entry name as folder.

          The 'exclude-fn' filters the files in the folder that are to be 
          excluded from the zip. 'exclude-fn' is a single argument function 
          that receives a file and returns true if the files is to be excluded 
          otherwise it returns false.
          """
     :examples '(
          """
          (do 
            (load-module :zipvault)

            (let [zip        (io/file "vault.zip")
                  tmp-folder (io/file (io/tmp-dir) "ziptest")
                  tmp-1      (io/file tmp-folder "a1.txt")
                  tmp-2      (io/file tmp-folder "a2.txt")]
              (io/mkdir tmp-folder)
              (io/spit tmp-1 "1234")
              (io/spit tmp-2 "2345")
              (io/delete-file-on-exit tmp-folder)

              (zipvault/zip-folder zip "pwd" tmp-folder)))
          """,
          """
          (do 
            (load-module :zipvault)

            (defn exclude-fn [file] (io/file-ext? file "log"))

            (let [zip        (io/file "vault.zip")
                  tmp-folder (io/file (io/tmp-dir) "ziptest")
                  tmp-1      (io/file tmp-folder "a.txt")
                  tmp-2      (io/file tmp-folder "b.txt")
                  tmp-3      (io/file tmp-folder "c.log")]
              (io/mkdir tmp-folder)
              (io/spit tmp-1 "12")
              (io/spit tmp-2 "23")
              (io/spit tmp-3 "34")
              (io/delete-file-on-exit tmp-folder)

              (zipvault/zip -folder zip "pwd" tmp-folder true exclude-fn)))
          """ )
     :see-also '(
          "zipvault/zip"
          "zipvault/add-folder" ) }

  zip-folder 

  ([out passphrase folder]
    (zip out passphrase)
    (add-folder out passphrase folder))

  ([out passphrase folder include-root-folder]
    (zip out passphrase)
    (add-folder out passphrase folder include-root-folder))

  ([out passphrase folder include-root-folder exclude-fn]
    (zip out passphrase)
    (add-folder out passphrase folder include-root-folder exclude-fn)))


(defn 
  ^{ :arglists '(
          "(zipvault/entropy passphrase)")
     :doc """
          Returns the passphrase's entropy in bits.

          The password entropy using the formula: **E = log2(RL)**

          * **E** stands for password entropy, measured in bits
          * **Log2** is a mathematical formula that converts the total number \
            of possible character combinations to bits
          * **R** stands for the range of characters
          * **L** stands for the number of characters in a password

          The entropy is calculated based on 26 lower and upper case letters,
          10 digits, and 24 symbols like °+*%&/()=?'`^:_,.-$£!#~;

          Note: The function just calculates the entropy. A strong passphrase 
                does not rely on the entropy solely. Avoid passphrases containing 
                words from the dictionary ("admin_passw0rd"), dates (birthdate, 
                ...), repetitions ("aaaaa"), or sequences ("123456")!
          """
     :examples '(
          """
          (do 
            (load-module :zipvault)
            (zipvault/entropy "uibsd6b38hs7b_La'sdgk898wbver"))
          """ )
     :see-also '(
          "zipvault/zip" ) }

  entropy [passphrase]

  { :pre [(string? passphrase)] } 

  (let [letters  26, digits  10, symbols 24]
    (log2 (pow (+ letters letters digits symbols) (count passphrase)))))


(defn 
  ^{ :arglists '(
          "(zipvault/entries zip passphrase)")
     :doc """
          Returns a list of the entry names in the zip.
          """
     :examples '(
          """
          (do 
            (load-module :zipvault)

            (zipvault/zip (io/file "vault.zip")
                          "pwd"
                          "a.txt"  "abc"
                          "b.txt"  "def")
            (zipvault/entries (io/file "vault.zip") "pwd"))
          """ )
     :see-also '(
          "zipvault/zip" ) }

  entries [zip passphrase]

  { :pre [(file-or-string? zip)
          (string? passphrase)] } 

  (try-with [zf (. :ZipFile :new (io/file zip) passphrase)]
    (->> (. zf :getFileHeaders)
         (map (fn [h] { :file-name         (. h :getFileName)
                        :compressed-size   (. h :getCompressedSize)
                        :uncompressed-size (. h :getUncompressedSize)
                        :encrypted?        (. h :isEncrypted)
                        :directory?        (. h :isDirectory) })))))


(defn 
  ^{ :arglists '("(zipvault/add-files zip passphrase & files)")
     :doc """
          Adds a list of files to the zip.
          """
     :examples '(
          """
          (do 
            (load-module :zipvault)

            (let [zip   (io/file "vault.zip")
                  tmp-1 (io/file (io/tmp-dir) "a1.txt")
                  tmp-2 (io/file (io/tmp-dir) "a2.txt")]
              (io/spit tmp-1 "1234")
              (io/spit tmp-2 "2345")
              (io/delete-file-on-exit tmp-1)
              (io/delete-file-on-exit tmp-2)

              (zipvault/zip zip "pwd" "a.txt" "A")
              (zipvault/add-files zip "pwd" tmp-1 tmp-2)))
          """ )
     :see-also '(
          "zipvault/zip"
          "zipvault/add-folder"
          "zipvault/add-stream"
          "zipvault/remove-files" ) }

  add-files [zip passphrase & files]

  { :pre [(file-or-string? zip)
          (string? passphrase)
          (every? file-or-string? files)] } 

  (let [params  (create-params)]
    (try-with [zf (. :ZipFile :new (io/file zip) passphrase)]
      (. zf :addFiles files params))))


(defn 
  ^{ :arglists '(
          "(zipvault/add-folder zip passphrase folder)"
          "(zipvault/add-folder zip passphrase folder include-root-folder)"
          "(zipvault/add-folder zip passphrase folder include-root-folder exclude-fn)")
     :doc """
          Adds a folder to the zip file. 
          
          If 'include-root-folder' (default true) is true the root folder name 
          will be added to the entry name as folder.

          The 'exclude-fn' filters the files in the folder that are to be 
          excluded from the zip. 'exclude-fn' is a single argument function 
          that receives a file and returns true if the files is to be excluded 
          otherwise it returns false.
          """
     :examples '(
          """
          (do 
            (load-module :zipvault)

            (let [zip        (io/file "vault.zip")
                  tmp-folder (io/file (io/tmp-dir) "ziptest")
                  tmp-1      (io/file tmp-folder "a1.txt")
                  tmp-2      (io/file tmp-folder "a2.txt")]
              (io/mkdir tmp-folder)
              (io/spit tmp-1 "1234")
              (io/spit tmp-2 "2345")
              (io/delete-file-on-exit tmp-folder)

              (zipvault/zip zip "pwd" "a.txt" "A")
              (zipvault/add-folder zip "pwd" tmp-folder)))
          """,
          """
          (do 
            (load-module :zipvault)

            (defn exclude-fn [file] (io/file-ext? file "log"))

            (let [zip        (io/file "vault.zip")
                  tmp-folder (io/file (io/tmp-dir) "ziptest")
                  tmp-1      (io/file tmp-folder "a.txt")
                  tmp-2      (io/file tmp-folder "b.txt")
                  tmp-3      (io/file tmp-folder "c.log")]
              (io/mkdir tmp-folder)
              (io/spit tmp-1 "12")
              (io/spit tmp-2 "23")
              (io/spit tmp-3 "34")
              (io/delete-file-on-exit tmp-folder)

              (zipvault/zip zip "pwd")
              (zipvault/add-folder zip "pwd" tmp-folder true exclude-fn)))
          """ )
     :see-also '(
          "zipvault/zip"
          "zipvault/add-files"
          "zipvault/add-stream"
          "zipvault/remove-files" ) }

  add-folder 

  ([zip passphrase folder]
    { :pre [(file-or-string? zip)
            (string? passphrase)
            (file-or-string? folder)] } 
    (add-folder (io/file zip) passphrase folder true))
  
  ([zip passphrase folder include-root-folder]
    { :pre [(file-or-string? zip)
            (string? passphrase)
            (file-or-string? folder)
            (boolean? include-root-folder)] } 
    (let [params  (create-params)]
      (. params :setIncludeRootFolder include-root-folder)
      (try-with [zf (. :ZipFile :new (io/file zip) passphrase)]
        (. zf :addFolder folder params))))
  
  ([zip passphrase folder include-root-folder exclude-fn]
    { :pre [(file-or-string? zip)
            (string? passphrase)
            (file-or-string? folder)
            (boolean? include-root-folder)
            (fn? exclude-fn)] } 
    (let [params   (create-params)
          exc-fn   (proxify :ExcludeFileFilter {:isExcluded exclude-fn})]
      (. params :setIncludeRootFolder include-root-folder)
      (. params :setExcludeFileFilter exc-fn)
      (try-with [zf (. :ZipFile :new (io/file zip) passphrase)]
        (. zf :addFolder folder params)))))


(defn 
  ^{ :arglists '("(zipvault/add-stream zip passphrase name is)")
     :doc """
          Creates a new entry in the zip file and adds the content of the 
          input stream to the zip file.
          """
     :examples '(
          """
          (do 
            (load-module :zipvault)

            (let [zip (io/file "vault.zip")
                  is  (io/string-in-stream "abc")]
              (zipvault/zip zip "pwd" "a.txt" "A")
              (zipvault/add-stream zip "pwd" "a.txt" is)))
          """ )
     :see-also '(
          "zipvault/zip"
          "zipvault/add-files"
          "zipvault/add-folder"
          "zipvault/remove-files" ) }

  add-stream [zip passphrase name is]

  { :pre [(file-or-string? zip)
          (string? passphrase)
          (string? name)
          (instance-of? :java.io.InputStream is)] } 

  (let [params  (create-params)]
    (. params :setFileNameInZip name)
    (try-with [zf (. :ZipFile :new (io/file zip) passphrase)]
      (. zf :addStream is params))))


(defn 
  ^{ :arglists '("(zipvault/remove-files zip passphrase & files)")
     :doc """
          Removes all files from the zip file that match the names in the input 
          list.

          If any of the file is a directory, all the files and directories under 
          this directory will be removed as well.
          """
     :examples '(
          """
          (do 
            (load-module :zipvault)

            (let [zip (io/file "vault.zip")]
              (zipvault/zip zip "pwd" "a.txt" "A" "b.txt" "B")
              (zipvault/remove-files zip "pwd" "a.txt")))
          """ )
     :see-also '(
          "zipvault/zip"
          "zipvault/add-files"
          "zipvault/add-folder"
          "zipvault/add-stream" ) }

  remove-files [zip passphrase & files]

  { :pre [(file-or-string? zip)
          (string? passphrase)
          (or (empty? files) (every? string? files))] } 

  (when-not (empty? files)
    (try-with [zf (. :ZipFile :new (io/file zip) passphrase)]
      (. zf :removeFiles files))))
 

(defn 
  ^{ :arglists '("(zipvault/encrypted? zip)")
     :doc """
          Extracts a specific file from the zip file to the destination path.
          """
     :examples '(
          """
          (do 
            (load-module :zipvault)

            (zipvault/zip (io/file "vault.zip") "pwd" "a.txt" "abc")
            (zipvault/encrypted? (io/file "vault.zip")))
          """ ) }

  encrypted? [zip]

  { :pre [(file-or-string? zip)] }

  (assert (io/exists-file? zip))
 
  (try-with [zf (. :ZipFile :new (io/file zip))]
    (. zf :isEncrypted)))


(defn 
  ^{ :arglists '("(zipvault/valid-zip-file? zip)")
     :doc """
          Returns true if the zip is a valid zip file else false.
          """
     :examples '(
          """
          (do 
            (load-module :zipvault)

            (zipvault/zip (io/file "vault.zip") "pwd" "a.txt" "abc")
            (zipvault/valid-zip-file? (io/file "vault.zip")))
          """ ) }

  valid-zip-file? [zip]

  { :pre [(file-or-string? zip)] }

  (try-with [zf (. :ZipFile :new (io/file zip))]
    (. zf :isValidZipFile)))


(defn 
  ^{ :arglists '("(zipvault/extract-file zip password filename destpath)")
     :doc """
          Extracts a specific file or folder from the zip file to the 
          destination path.
          """
     :examples '(
          """
          (do 
            (load-module :zipvault)

            (zipvault/zip (io/file "vault.zip")
                          "pwd"
                          "a.txt" "abc"
                          "b.txt" "def")

            ;; extract a file
            (zipvault/extract-file (io/file "vault.zip")
                                   "pwd"
                                   "a.txt"
                                   "."))
          """,
          """
          (do 
            (load-module :zipvault)

            (zipvault/zip (io/file "vault.zip")
                          "pwd"
                          "words/one.txt" "one"
                          "words/two.txt" "two"
                          "logs/001.log" "xxx")

            ;; extract a folder
            (zipvault/extract-file (io/file "vault.zip")
                                   "pwd"
                                   "words/"
                                   "."))
          """ )
     :see-also '(
          "zipvault/zip"
          "zipvault/extract-all"
          "zipvault/extract-file-data" ) }

  extract-file [zip passphrase filename destpath]

  { :pre [(file-or-string? zip)
          (string? passphrase)
          (string? filename)
          (or (string? destpath) (io/file? destpath))] }

  (assert (io/exists-file? zip))
  (assert (io/file-can-read? zip))
  (assert (io/exists-dir? destpath))
  (assert (io/file-can-write? destpath))

  (let [destpath (if (string? destpath) destpath (io/file-path destpath))]
    (try-with [zf (. :ZipFile :new (io/file zip) passphrase)]
      (. zf :extractFile filename destpath))))


(defn 
  ^{ :arglists '(
          "(zipvault/extract-all zip destpath)"
          "(zipvault/extract-all zip passphrase destpath)")
     :doc """
          Extracts all files from the zip file to the destination path.
          """
     :examples '(
          """
          (do 
            (load-module :zipvault)

            (zipvault/zip (io/file "vault.zip")
                          "pwd"
                          "a.txt" "abc"
                          "b.txt" "def")

            (zipvault/extract-all (io/file "vault.zip")
                                  "pwd"
                                  "."))
          """ )
     :see-also '(
          "zipvault/zip"
          "zipvault/extract-file"
          "zipvault/extract-file-data" ) }

  extract-all 
   
  ([zip destpath]
    { :pre [(file-or-string? zip) (file-or-string? destpath)] }

    (assert (io/exists-file? zip))
    (assert (io/file-can-read? zip))
    (assert (io/exists-dir? destpath))
    (assert (io/file-can-write? destpath))

    (let [destpath (if (string? destpath) destpath (io/file-path destpath))]
      (try-with [zf (. :ZipFile :new (io/file zip))]
        (. zf :extractAll destpath))))
 
  ([zip passphrase destpath]
    { :pre [(file-or-string? zip)
            (string? passphrase)
            (file-or-string? destpath)] }

    (assert (io/exists-file? zip))
    (assert (io/file-can-read? zip))
    (assert (io/exists-dir? destpath))
    (assert (io/file-can-write? destpath))

    (let [destpath (if (string? destpath) destpath (io/file-path destpath))]
      (try-with [zf (. :ZipFile :new (io/file zip) passphrase)]
        (. zf :extractAll destpath)))))




(defn 
  ^{ :arglists '("(zipvault/extract-file-data in passphrase filename)")
     :doc """
          Extracts a specific file from the zip file and returns it as binary 
          data. in may be a file or an input stream.

          Returns `nil` if the file does not exist.
          """
     :examples '(
          """
          (do 
            (load-module :zipvault)

            (zipvault/zip (io/file "vault.zip")
                          "pwd"
                          "a.txt" "abc"
                          "b.txt" "def")

            (zipvault/extract-file-data (io/file "vault.zip")
                                        "pwd"
                                        "a.txt"))
          """ )
     :see-also '(
          "zipvault/zip"
          "zipvault/extract-file"
          "zipvault/extract-all" ) }

  extract-file-data [in passphrase filename]

  { :pre [(or (file-or-string? in) (instance-of? :java.io.InputStream in))
          (string? passphrase)
          (string? filename)] }

  (let [is (if (file-or-string? in) (io/file-in-stream in) in)]
    (try-with [zis (. :ZipInputStream :new is passphrase)] 
      (loop [header (. zis :getNextEntry)]
        (if (nil? header)
          nil  ; file not found -> return no data
          (let [name (. header :getFileName)]
            (if (= filename name)
              (io/slurp zis :binary true)  ; return binary file data
              (recur (. zis :getNextEntry)))))))))
 


(defn- zip-to-os [params passphrase os entries]
  (try-with [zos (. :ZipOutputStream :new os passphrase)]  
    (docoll (fn [[k v]] (add-entry params zos k v)) entries)))

(defn- add-entry [params zos name value]
  (let [name (if (io/file? name) (io/file-name name) name)]
    (cond
      (str/ends-with? name "/")
        (add-entry-folder params zos name)

      (nil? value)
        (add-entry-data params zos name (bytebuf))

      (fn? value)
        (add-entry-producer params zos name value)

      (instance-of? :java.io.InputStream value)
        (add-entry-data params zos name (io/slurp value :binary true))

      (= (type value) :java.io.File)
        (add-entry-data params zos name (io/slurp value :binary true))

      (= (type value) :core/string)
        (add-entry-data params zos name (bytebuf value))

      (= (type value) :core/bytebuf)
        (add-entry-data params zos name value)

      :else
        (throw (ex :VncException 
                    """
                    Invalid zip entry value type ~(type value)! \
                    Expected a file, string, or bytebuf.
                    """)))))

(defn- add-entry-folder [params zos name]
  (. params :setFileNameInZip name)
  (. zos :putNextEntry params)
  (. zos :closeEntry))

(defn- add-entry-data [params zos name data]
  (. params :setFileNameInZip name)
  (. zos :putNextEntry params)
  (. zos :write data)
  (. zos :closeEntry))

(defn- add-entry-producer [params zos name producer]
  (. params :setFileNameInZip name)
  (. zos :putNextEntry params)
  (producer zos)
  (. zos :closeEntry))

(defn- create-params []
  (doto (. :ZipParameters :new)
        (. :setEncryptFiles true)
        (. :setEncryptionMethod :AES)
        (. :setAesKeyStrength :KEY_STRENGTH_256)
        (. :setAesVersion :TWO)
        (. :setCompressionLevel :NORMAL)))

(defn- file-or-string? [x]
  (or (io/file? x) (string? x)))




© 2015 - 2025 Weber Informatics LLC | Privacy Policy