class Origami::Encryption::Standard::Dictionary

Class defining a standard encryption dictionary.

Constants

O
OE
Perms
U
UE

Public Instance Methods

compute_legacy_user_encryption_key(user_password, file_id) click to toggle source

Computes the key that will be used to encrypt/decrypt the document contents. Only for Revision 4 and less.

# File lib/origami/encryption.rb, line 813
def compute_legacy_user_encryption_key(user_password, file_id)
    padded = pad_password(user_password)
    padded.force_encoding('binary')

    padded << self.O
    padded << [ self.P ].pack("i")

    padded << file_id

    encrypt_metadata = self.EncryptMetadata != false
    padded << [ -1 ].pack("i") if self.R >= 4 and not encrypt_metadata

    key = Digest::MD5.digest(padded)

    50.times { key = Digest::MD5.digest(key[0, self.Length / 8]) } if self.R >= 3

    truncate_key(key)
end
compute_owner_encryption_key(owner_password) click to toggle source

Computes the key that will be used to encrypt/decrypt the document contents with owner password. Revision 5 and above.

# File lib/origami/encryption.rb, line 836
def compute_owner_encryption_key(owner_password)
    return if self.R < 5

    passwd = password_to_utf8(owner_password)
    oks = self.O[40, 8]

    if self.R == 5
        okey = Digest::SHA256.digest(passwd + oks + self.U)
    else
        okey = compute_hardened_hash(passwd, oks, self.U)
    end

    iv = ::Array.new(AES::BLOCKSIZE, 0).pack("C*")
    AES.new(okey, nil, false).decrypt(iv + self.OE.value)
end
compute_user_encryption_key(user_password, file_id) click to toggle source

Computes the key that will be used to encrypt/decrypt the document contents with user password. Called at all revisions.

# File lib/origami/encryption.rb, line 792
def compute_user_encryption_key(user_password, file_id)
    return compute_legacy_user_encryption_key(user_password, file_id) if self.R < 5

    passwd = password_to_utf8(user_password)

    uks = self.U[40, 8]

    if self.R == 5
        ukey = Digest::SHA256.digest(passwd + uks)
    else
        ukey = compute_hardened_hash(passwd, uks)
    end

    iv = ::Array.new(AES::BLOCKSIZE, 0).pack("C*")
    AES.new(ukey, nil, false).decrypt(iv + self.UE.value)
end
derive_encryption_key(passwd, doc_id) click to toggle source

Checks the given password and derives the document encryption key. Raises EncryptionInvalidPasswordError on invalid password.

# File lib/origami/encryption.rb, line 773
def derive_encryption_key(passwd, doc_id)
    if is_user_password?(passwd, doc_id)
        compute_user_encryption_key(passwd, doc_id)
    elsif is_owner_password?(passwd, doc_id)
        if self.V.to_i < 5
            user_passwd = retrieve_user_password(passwd)
            compute_user_encryption_key(user_passwd, doc_id)
        else
            compute_owner_encryption_key(passwd)
        end
    else
        raise EncryptionInvalidPasswordError
    end
end
is_owner_password?(pass, salt) click to toggle source

Checks owner password. For version 2,3 and 4, salt is the document ID. For version 5, salt is (Owner Key Salt + U)

# File lib/origami/encryption.rb, line 932
def is_owner_password?(pass, salt)

    if self.R < 5
        user_password = retrieve_user_password(pass)
        is_user_password?(user_password, salt)
    elsif self.R == 5
        ovs = self.O[32, 8]
        Digest::SHA256.digest(password_to_utf8(pass) + ovs + self.U) == self.O[0, 32]
    elsif self.R == 6
        ovs = self.O[32, 8]
        compute_hardened_hash(password_to_utf8(pass), ovs, self.U[0,48]) == self.O[0, 32]
    end
end
is_user_password?(pass, salt) click to toggle source

Checks user password. For version 2, 3 and 4, salt is the document ID. For version 5 and 6, salt is the User Key Salt.

# File lib/origami/encryption.rb, line 912
def is_user_password?(pass, salt)

    if self.R == 2
        compute_user_password_hash(pass, salt) == self.U
    elsif self.R == 3 or self.R == 4
        compute_user_password_hash(pass, salt)[0, 16] == self.U[0, 16]
    elsif self.R == 5
        uvs = self.U[32, 8]
        Digest::SHA256.digest(password_to_utf8(pass) + uvs) == self.U[0, 32]
    elsif self.R == 6
        uvs = self.U[32, 8]
        compute_hardened_hash(password_to_utf8(pass), uvs) == self.U[0, 32]
    end
end
retrieve_user_password(owner_password) click to toggle source

Retrieve user password from owner password. Cannot be used with revision 5.

# File lib/origami/encryption.rb, line 950
def retrieve_user_password(owner_password)

    key = compute_owner_key(owner_password)

    if self.R == 2
        RC4.decrypt(key, self.O)
    elsif self.R == 3 or self.R == 4
        user_password = RC4.decrypt(xor(key, 19), self.O)
        19.times { |i| user_password = RC4.decrypt(xor(key, 18-i), user_password) }

        user_password
    end
end
set_legacy_passwords(owner_password, user_password, salt) click to toggle source

Set up document passwords. Only for Revision 4 and less.

# File lib/origami/encryption.rb, line 896
def set_legacy_passwords(owner_password, user_password, salt)
    owner_key = compute_owner_key(owner_password)
    upadded = pad_password(user_password)

    owner_key_hash = RC4.encrypt(owner_key, upadded)
    19.times { |i| owner_key_hash = RC4.encrypt(xor(owner_key, i + 1), owner_key_hash) } if self.R >= 3

    self.O = owner_key_hash
    self.U = compute_user_password_hash(user_password, salt)
end
set_passwords(owner_password, user_password, salt = nil) click to toggle source

Set up document passwords.

# File lib/origami/encryption.rb, line 855
def set_passwords(owner_password, user_password, salt = nil)
    return set_legacy_passwords(owner_password, user_password, salt) if self.R < 5

    upass = password_to_utf8(user_password)
    opass = password_to_utf8(owner_password)

    uvs, uks, ovs, oks = ::Array.new(4) { Encryption.rand_bytes(8) }
    file_key = Encryption.strong_rand_bytes(32)
    iv = ::Array.new(AES::BLOCKSIZE, 0).pack("C*")

    if self.R == 5
        self.U = Digest::SHA256.digest(upass + uvs) + uvs + uks
        self.O = Digest::SHA256.digest(opass + ovs + self.U) + ovs + oks
        ukey = Digest::SHA256.digest(upass + uks)
        okey = Digest::SHA256.digest(opass + oks + self.U)
    else
        self.U = compute_hardened_hash(upass, uvs) + uvs + uks
        self.O = compute_hardened_hash(opass, ovs, self.U) + ovs + oks
        ukey = compute_hardened_hash(upass, uks)
        okey = compute_hardened_hash(opass, oks, self.U)
    end

    self.UE = AES.new(ukey, iv, false).encrypt(file_key)[iv.size, 32]
    self.OE = AES.new(okey, iv, false).encrypt(file_key)[iv.size, 32]

    perms =
        [ self.P ].pack("V") +                              # 0-3
        [ -1 ].pack("V") +                                  # 4-7
        (self.EncryptMetadata == true ? "T" : "F") +        # 8
        "adb" +                                             # 9-11
        [ 0 ].pack("V")                                     # 12-15

    self.Perms = AES.new(file_key, iv, false).encrypt(perms)[iv.size, 16]

    file_key
end

Private Instance Methods

compute_hardened_hash(password, salt, vector = '') click to toggle source

Computes hardened hash used in revision 6 (extension level 8).

# File lib/origami/encryption.rb, line 1007
def compute_hardened_hash(password, salt, vector = '')
    block_size = 32
    input = Digest::SHA256.digest(password + salt + vector) + "\x00" * 32
    key = input[0, 16]
    iv = input[16, 16]
    digest, aes, h, x = nil, nil, nil, nil

    i = 0
    while i < 64 or i < x[-1].ord + 32

        block = input[0, block_size]

        aes = OpenSSL::Cipher.new("aes-128-cbc").encrypt
        aes.iv = iv
        aes.key = key
        aes.padding = 0

        64.times do |j|
            x = ''
            x += aes.update(password) unless password.empty?
            x += aes.update(block)
            x += aes.update(vector) unless vector.empty?

            if j == 0
                block_size = 32 + (x.unpack("C16").inject(0) {|a,b| a+b} % 3) * 16
                digest = Digest::SHA2.new(block_size << 3)
            end

            digest.update(x)
        end

        h = digest.digest
        key = h[0, 16]
        input[0, block_size] = h[0, block_size]
        iv = h[16, 16]

        i = i + 1
    end

    h[0, 32]
end
truncate_key(key) click to toggle source

Some revision handlers require different key sizes. Revision 2 uses 40-bit keys. Revisions 3 and higher rely on the Length field for the key size.

# File lib/origami/encryption.rb, line 1054
def truncate_key(key)
    if self.R == 2
        key[0, 5]
    elsif self.R >= 3
        key[0, self.Length / 8]
    end
end