Module: OAK
- Defined in:
- lib/oak.rb,
lib/oak/version.rb
Defined Under Namespace
Classes: CantTouchThisObjectError, CantTouchThisStringError, Key, KeyChain
Constant Summary collapse
- BAD_OBJ =
Internal syntactic conveniences.
CantTouchThisObjectError
- BAD_STR =
CantTouchThisStringError
- ENCRYPTION_ALGO_NAME =
OAK_4 supports one and only one encryption algorithm and mode of operation.
- AES-256-GCM - 128 bits of security - 256-bit keys (32 bytes) - 96-bit IVs (12 bytes) - 128-bit auth_tags (16 bytes) - Random IV ("Initialization Vector") for each encryption op - All headers authenticated. - Headers encrypted when not required for decryption.
'aes-256-gcm'.freeze
- ENCRYPTION_ALGO_IV_BYTES =
AES-256-GCM has 96-bit IVs
12
- ENCRYPTION_ALGO_AUTH_TAG_BYTES =
AES-256-GCM has 128-bit auth, we use all
16
- VERSION =
'0.4.2'.freeze
Class Method Summary collapse
-
._check(redundancy, str) ⇒ Object
Helper method, calculates redundancy check for str.
-
._compress(compression, force, str) ⇒ Object
Helper for wrap() and unwrap(), multiplexes compression.
-
._decompress(compression, str) ⇒ Object
Helper for wrap() and unwrap(), multiplexes decompression.
-
._decrypt(encryption_key, data, auth_data) ⇒ Object
Helper for wrap() and unwrap(), multiplexes decryption.
- ._deformat(format, str) ⇒ Object
-
._deserialize(str) ⇒ Object
Deserializes suitable naive strings into objects.
-
._encrypt(encryption_key, data, auth_data, debug_iv) ⇒ Object
Helper for wrap() and unwrap(), multiplexes encryption.
-
._format(format, str) ⇒ Object
Helper method, calculates formatted version of str.
-
._safety_dance(obj, seen = nil, reseen = nil, &block) {|obj| ... } ⇒ Object
Walks obj recursively, touching each reachable child only once without getting caught up cycles or touching DAGy bits twice.
-
._serialize(obj) ⇒ Object
Serializes suitable objects string into naive strings.
-
._unwrap(str, opts = {}) ⇒ Object
Unwraps any OAK string into a string.
- ._unwrap_oak_3(sc) ⇒ Object
- ._unwrap_oak_4(sc, opts = {}) ⇒ Object
-
._wrap(str, opts = {}) ⇒ Object
Wraps any string into a OAK string.
- ._wrap_oak_3(str, redundancy, compression, force, format) ⇒ Object
- ._wrap_oak_4(str, redundancy, compression, force, format, key, encryption_key, debug_iv) ⇒ Object
-
.decode(str, opts = {}) ⇒ Object
Decodes suitable OAK strings into objects.
-
.encode(obj, opts = {}) ⇒ Object
Encodes suitable objects string into OAK strings.
-
.encryption_algo ⇒ Object
Get a new instance of OpenSSL::Cipher for our algorithm.
-
.parse_env_chain(env, name) ⇒ Object
Parses a KeyChain object and keys from an ENV-like object.
-
.random_iv ⇒ Object
Generate a new random initialization vector appropriate for the OAK_4 encryption algorithm.
-
.random_key ⇒ Object
Generate a new random key appropriate for the OAK_4 encryption algorithm.
Class Method Details
._check(redundancy, str) ⇒ Object
Helper method, calculates redundancy check for str.
846 847 848 849 850 851 852 853 854 |
# File 'lib/oak.rb', line 846 def self._check(redundancy,str) case redundancy.to_s when 'none' then return '0' when 'crc32' then return '%d' % Zlib.crc32(str) when 'sha1' then return Digest::SHA1.hexdigest(str) else raise ArgumentError, "unknown redundancy #{redundancy}" end end |
._compress(compression, force, str) ⇒ Object
Helper for wrap() and unwrap(), multiplexes compression.
983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 |
# File 'lib/oak.rb', line 983 def self._compress(compression,force,str) case compression.to_s when 'none' compressed = str when 'lz4' compressed = LZ4.compress(str) when 'zlib' compressed = Zlib.deflate(str) when 'bzip2' io = StringIO.new io.set_encoding(Encoding::ASCII_8BIT) Bzip2::FFI::Writer.write(io, str) compressed = io.string when 'lzma' compressed = LZMA.compress(str) else raise ArgumentError, "unknown compression #{compression}" end if !force && compressed.size >= str.size compressed = str compression = 'none' end [compressed,compression.to_s] end |
._decompress(compression, str) ⇒ Object
Helper for wrap() and unwrap(), multiplexes decompression.
1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 |
# File 'lib/oak.rb', line 1010 def self._decompress(compression,str) case compression.to_s when 'none' return str when 'lz4' begin return LZ4.uncompress(str) rescue LZ4Internal::Error => ex raise CantTouchThisStringError, "#{ex.class}: #{ex.}" end when 'zlib' begin return Zlib::Inflate.inflate(str) rescue Zlib::DataError => ex raise CantTouchThisStringError, "#{ex.class}: #{ex.}" end when 'bzip2' io = StringIO.new(str) raw = nil begin raw = Bzip2::FFI::Reader.read(io) rescue Bzip2::FFI::Error::MagicDataError => ex raise CantTouchThisStringError, "#{ex.class}: #{ex.}" end str = raw.b # dupe to Encoding::ASCII_8BIT return str when 'lzma' begin raw = LZMA.decompress(str) rescue RuntimeError => ex raise CantTouchThisStringError, "#{ex.class}: #{ex.}" end str = raw.b # dupe to Encoding::ASCII_8BIT return str else raise ArgumentError, "unknown compression #{compression}" end end |
._decrypt(encryption_key, data, auth_data) ⇒ Object
Helper for wrap() and unwrap(), multiplexes decryption.
949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 |
# File 'lib/oak.rb', line 949 def self._decrypt(encryption_key,data,auth_data) return data if !encryption_key iv_size = ENCRYPTION_ALGO_IV_BYTES auth_tag_size = ENCRYPTION_ALGO_AUTH_TAG_BYTES iv = data[0..(iv_size-1)] auth_tag = data[iv_size..(auth_tag_size+iv_size-1)] ciphertext = data[(auth_tag_size+iv_size)..-1] cipher = encryption_algo.decrypt cipher.key = encryption_key.key begin cipher.iv = iv cipher.auth_tag = auth_tag cipher.auth_data = auth_data cipher.update(ciphertext) + cipher.final rescue ArgumentError => ex # # Some of our tests of corrupting OAK strings lead to incorrect # parses which cause the data passed to this method to be # shorter than ENCRYPTION_ALGO_IV_BYTES. # # In ruby <= 2.2.7 (w/ openssl 1.1.0), these truncated IVs # result in OpenSSL::Cipher::CipherError from cipher.update(). # # In ruby >= 2.4.3 (w/ openssl 2.0.5), truncated IVs result in # ArgumentError in cipher.iv=(). # raise CantTouchThisStringError, "#{ex.class}: #{ex.}" rescue OpenSSL::Cipher::CipherError => ex raise CantTouchThisStringError, "#{ex.class}: #{ex.}" end end |
._deformat(format, str) ⇒ Object
877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 |
# File 'lib/oak.rb', line 877 def self._deformat(format,str) case format.to_s when 'none' return str when 'base64' # # Regrettably, Base64.urlsafe_decode64(str) does not reverse # Base64.urlsafe_encode64(str).gsub(/=.*$/,''), it raises an # ArgumentError "invalid base64". # # Fortunately, simple Base64.decode64() is liberal in what it # accepts, and handles the output of all of encode64, # strict_encode64, and urlsafe_encode64 both with and without # the /=*$/. # return Base64.decode64(str.tr('-_','+/')) else raise ArgumentError, "unknown format #{format}" end end |
._deserialize(str) ⇒ Object
Deserializes suitable naive strings into objects.
Inverts serialize().
370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 |
# File 'lib/oak.rb', line 370 def self._deserialize(str) scanner = StringScanner.new(str) serial_code = scanner.scan(/F/) if 'F' != serial_code raise CantTouchThisStringError, "bogus serial_code #{serial_code}" end num_objs = scanner.scan(/[0-9]+/) if !num_objs raise CantTouchThisStringError, "missing object list size" end num_objs = num_objs.to_i strt = Hash.new # string table, id => str for strings already decoded seen = [] # # We parse the stream, constructing all the objects we see in to # a seen list. # # In this first pass, Arrays and Hashes are created whose # elements, keys, and values are temporarily integers. These all # refer to slots in the seen list, and many of them will be # forward references to objects which we have yet to decode. # Later we will rectify the object graph by replacing these # integers with their refrants from the seen list. # num_objs.times.each do |idx_obj| code = scanner.scan(/[a-zA-Z]/) case code when 'n' seen[idx_obj] = nil when 'f' seen[idx_obj] = false when 't' seen[idx_obj] = true when 'S', 'Y', 's', 'y' enc_code = scanner.scan(/[AU]/) enc = nil case enc_code when 'A' enc = Encoding::ASCII_8BIT when 'U' enc = Encoding::UTF_8 else raise CantTouchThisStringError, "unknown enc_code #{enc_code}" end num = scanner.scan(/[0-9]+/) if !num raise CantTouchThisStringError, "missing num" end num = num.to_i case code when 'S', 'Y' if num > 0 scanner.scan(/_/) or raise BAD_STR, "missing _" seen[idx_obj] = scanner.peek(num) scanner.pos += num # skip body else seen[idx_obj] = '' end strt[strt.size] = seen[idx_obj] when 's', 'y' seen[idx_obj] = strt[num] end seen[idx_obj] = seen[idx_obj].dup.force_encoding(enc) case code when 'Y', 'y' seen[idx_obj] = seen[idx_obj].intern end when 'I' pattern = /-?[0-9]+/ seen[idx_obj] = scanner.scan(pattern).to_i when 'F' pattern = /-?(Infinity|NaN|[0-9]+(\.[0-9]*)?(e([+-][0-9]*)?)?)/ match = scanner.scan(pattern) case match when 'Infinity' then seen[idx_obj] = Float::INFINITY when '-Infinity' then seen[idx_obj] = -Float::INFINITY when 'NaN' then seen[idx_obj] = Float::NAN else seen[idx_obj] = match.to_f end when 'A' num_items = scanner.scan(/[0-9]+/).to_i arr = [] num_items.times.each do |idx| scanner.scan(/_/) or raise BAD_STR, "missing _" val = scanner.scan(/[0-9]+/).to_i # temp obj arr[idx] = val end seen[idx_obj] = arr when 'H' num_items = scanner.scan(/[0-9]+/).to_i hash = Hash.new num_items.times.each do scanner.scan(/_/) or raise BAD_STR, "missing _" k = scanner.scan(/[0-9]+/).to_i # temp obj scanner.scan(/_/) or raise BAD_STR, "missing _" v = scanner.scan(/[0-9]+/).to_i # temp obj hash[k] = v end seen[idx_obj] = hash else raise BAD_STR, "not handled: #{code} #{scanner.pos} #{scanner.rest}" end end # # If we parsed correctly, there will be no unconsumed in the # scanner. # if !scanner.eos? raise BAD_STR, "not at end-of-string: #{scanner.pos} #{scanner.rest}" end # # We rectify the references for each intermediate Array and Hash # as promised earlier. # # Note that this code must be inherently mutation-oriented since # it might have to construct cyclic graphs. # rectified = seen.map do |elem| if elem.is_a?(Array) next Array.new elsif elem.is_a?(Hash) next Hash.new else elem end end rectified.each_with_index do |elem,idx| if elem.is_a?(Array) seen[idx].each_with_index do |a,i| elem[i] = rectified[a] end elsif elem.is_a?(Hash) seen[idx].each do |k,v| elem[rectified[k]] = rectified[v] end end end # # By the way _safety_dance performed its walk in _serialize(), the # object we are decoding is the first object encoded in str. # # Thus, we return the first element of the rectified list. # rectified.first end |
._encrypt(encryption_key, data, auth_data, debug_iv) ⇒ Object
Helper for wrap() and unwrap(), multiplexes encryption.
900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 |
# File 'lib/oak.rb', line 900 def self._encrypt(encryption_key,data,auth_data,debug_iv) return data if !encryption_key # # WARNING: In at least some versions of OpenSSL::Cipher, setting # iv before key would cause the iv to be ignored in aes-*-gcm # ciphers! # # https://github.com/attr-encrypted/encryptor/pull/22 # https://github.com/attr-encrypted/encryptor/blob/master/README.md # # The issue was reported against version "1.0.1f 6 Jan 2014". I # have yet to figure out whether our current version, 1.1.0, is # affected, or when/how the fix will go live. # # OAK_4 only supports AES-256-GCB. Although the implementation # bug has been fixed and OAK will almost certainly not be used # with a buggy version of OpenSSL, nevertheless we take great # care to set cipher.key *then* cipher.iv. # # Still, can't be to careful. # iv_size = ENCRYPTION_ALGO_IV_BYTES auth_tag_size = ENCRYPTION_ALGO_AUTH_TAG_BYTES if debug_iv && iv_size != debug_iv.size raise "unexpected debug_iv.size #{debug_iv.size} not #{iv_size}" end cipher = encryption_algo.encrypt cipher.key = encryption_key.key iv = debug_iv || cipher.random_iv cipher.iv = iv cipher.auth_data = auth_data ciphertext = cipher.update(data) + cipher.final auth_tag = cipher.auth_tag if iv_size != iv.size raise "unexpected iv.size #{iv.size} not #{iv_size}" end if auth_tag_size != auth_tag.size raise "unexpected auth_tag.size #{auth_tag.size} not #{auth_tag_size}" end # # Since iv and auth_tag have fixed widths, they are trivial to # parse without putting any effort or space into recording their # sizes in the message body. # iv + auth_tag + ciphertext end |
._format(format, str) ⇒ Object
Helper method, calculates formatted version of str.
858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 |
# File 'lib/oak.rb', line 858 def self._format(format,str) case format.to_s when 'none' return str when 'base64' # # We actual using "Base 64 Encoding with URL and Filename Safe # Alphabet" aka base64url with the option not to use padding, # per https://tools.ietf.org/html/rfc4648#section-5. # # If we were using Ruby 2.3+, we could use the option "padding: # false" instead of chopping out the /=*$/ with gsub. # return Base64.urlsafe_encode64(str).gsub(/=.*$/,'') else raise ArgumentError, "unknown format #{format}" end end |
._safety_dance(obj, seen = nil, reseen = nil, &block) {|obj| ... } ⇒ Object
Walks obj recursively, touching each reachable child only once without getting caught up cycles or touching DAGy bits twice.
Only knows how to recurse into Arrays and Hashs.
This traversal is depth-first pre-order with the children of Arrays walked in positional anbd Hash pairs walked in positional order k,v,k,v, etc.
object touched, where idx is 0,1,2,… corresponding to the order in which we encountered child.
1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 |
# File 'lib/oak.rb', line 1070 def self._safety_dance(obj,seen=nil,reseen=nil,&block) # # Note that OAK._serialize() depends on the depth-first pre-order # specification here - at least, it assumes that the first element # walked will be the first element added to seen. # seen ||= {} reseen ||= [] oid = obj.object_id if seen.has_key?(oid) reseen << obj return seen,reseen end seen[oid] = [seen.size,obj] yield obj if block # pre-order: this node before children if obj.is_a?(Hash) obj.each do |k,v| # children in hash order and k,v,... _safety_dance(k,seen,reseen,&block) _safety_dance(v,seen,reseen,&block) end elsif obj.is_a?(Array) obj.each do |v| # children in list order _safety_dance(v,seen,reseen,&block) end end return seen,reseen end |
._serialize(obj) ⇒ Object
Serializes suitable objects string into naive strings.
Is inverted by deserialize(). For all obj, if serialize(obj) does not raise an exception, deserialize(serialize(obj)) == obj.
structure which cannot be encoded reversibly by OAK.
253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 |
# File 'lib/oak.rb', line 253 def self._serialize(obj) seen,_reseen = _safety_dance(obj) do |child| next if ALL_TYPES.select{ |type| child.is_a?(type) }.size > 0 raise CantTouchThisObjectError, "#{child.class} not supported: #{child}" end strt = Hash.new # string table, str => id for strings already encoded ser = 'F' ser << seen.size.to_s seen.each_with_index do |(_object_id,(_idx2,child)),_idx| # # First, identify the unique apex type in TYPE_2_CODE.keys # which matches the child. # # child.class may not be listed explicitly, such as for Fixnum # and Bigint both being Integer, so we search and assert # uniqueness and existence. # is_as = ALL_TYPES.select{ |type| child.is_a?(type) } raise CantTouchThisObjectError if 1 != is_as.size type = is_as[0] typecode = TYPE_2_CODE[type] if nil == child || true == child || false == child # # The type code by itself is sufficient to decode NilType, # TrueType, and FalseType. We need use other space for them. # ser << typecode next end if child.is_a?(Symbol) || child.is_a?(String) # # Strings and Symbols encode as their size in chars followed # by their bytes. # # We maintain a running string table, strt, to recognize when # we encounter a string representation which has been # previously encoded. # # If we find such a duplicate, we encode the current string # via a back reference to the first one we saw. This is # indicated by downcasing the typecode. # str = child.to_s enc = str.encoding enc_code = nil case enc when Encoding::ASCII_8BIT, Encoding::US_ASCII, Encoding::ASCII enc_code = 'A' when Encoding::UTF_8 enc_code = 'U' else raise CantTouchThisObjectError, "unknown string encoding #{enc}" end if strt.has_key?(str) ser << typecode.downcase # downcase indicates strt reference ser << enc_code ser << strt[str].to_s else ser << typecode # upcase indicates full representation ser << enc_code ser << str.bytesize.to_s if str.bytesize > 0 ser << '_' ser << str end strt[str] = strt.size end next end if child.is_a?(Numeric) # # Numerics primitives encode as their Ruby to_s which # matches their JSON.dump(). # ser << typecode ser << child.to_s next end if child.is_a?(Array) # # An array is encoded as a size N followed by N indexes into # the seen list. # ser << typecode ser << child.size.to_s child.each do |a| ser << '_' ser << seen[a.object_id][0].to_s end next end if child.is_a?(Hash) # # An array is encoded as a size N followed by 2*N indexes # into the seen list, organized pairwise key+value. # ser << typecode ser << child.size.to_s child.each do |k,v| ser << '_' ser << seen[k.object_id][0].to_s ser << '_' ser << seen[v.object_id][0].to_s end next end raise CantTouchThisObjectError, "not handled: #{child.class} #{child}" end ser end |
._unwrap(str, opts = {}) ⇒ Object
Unwraps any OAK string into a string.
Inverts wrap(). For all str, unwrap(wrap(str)) == str.
decrypt encrypted OAK strings, or nil for none.
682 683 684 685 686 687 688 689 690 691 |
# File 'lib/oak.rb', line 682 def self._unwrap(str,opts={}) str = str.b # str.b for dup to ASCII_8BIT sc = StringScanner.new(str) ov = sc.scan(/oak_[34]/) or raise BAD_STR, "bad oak+ver" if 'oak_4' == ov _unwrap_oak_4(sc,opts) # encryption opts possible for decoding OAK_4 :( else _unwrap_oak_3(sc) # no opts for decoding OAK_3 :) end end |
._unwrap_oak_3(sc) ⇒ Object
693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 |
# File 'lib/oak.rb', line 693 def self._unwrap_oak_3(sc) r = sc.scan(/[NCS]/) or raise BAD_STR, "bad redundancy" c = sc.scan(/[N4ZBM]/) or raise BAD_STR, "bad compression" f = sc.scan(/[NB]/) or raise BAD_STR, "bad format" _ = sc.scan(/_/) or raise BAD_STR, "missing _" scheck = sc.scan(/[a-f0-9]+/) or raise BAD_STR, "bad scheck" _ = sc.scan(/_/) or raise BAD_STR, "missing _" fsize = sc.scan(/[0-9]+/) or raise BAD_STR, "bad fsize" fsize = fsize.to_i _ = sc.scan(/_/) or raise BAD_STR, "missing _" formatted = sc.peek(fsize) begin sc.pos += fsize rescue RangeError => ex raise CantTouchThisStringError, "#{ex.class}: #{ex.}" end _ = sc.scan(/_ok$/) or raise BAD_STR, "bad ok: #{formatted}" redundancy = CODE_2_REDUNDANCY[r] || r compression = CODE_2_COMPRESSION[c] || c format = CODE_2_FORMAT[f] || f fsize_re = formatted.size if fsize.to_i != fsize_re raise CantTouchThisStringError, "fsize #{fsize} vs #{fsize_re}" end compressed = _deformat(format,formatted) original = _decompress(compression,compressed) scheck_re = _check(redundancy,original) if scheck != scheck_re raise CantTouchThisStringError, "scheck #{scheck} vs #{scheck_re}" end original end |
._unwrap_oak_4(sc, opts = {}) ⇒ Object
726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 |
# File 'lib/oak.rb', line 726 def self._unwrap_oak_4(sc,opts={}) key = sc.scan(/[^_]+/) # nil OK, indicates no compression encryption_key = nil if key key_chain = opts[:key_chain] if !key_chain raise CantTouchThisStringError, "key #{key} but no key_chain" end encryption_key = opts[:key_chain].keys[key] if !encryption_key keys = key_chain.keys raise CantTouchThisStringError, "key not found in #{keys}: #{key}" end end _ = sc.scan(/_/) or raise BAD_STR, "missing _" f = sc.scan(/[NB]/) or raise BAD_STR, "bad format" header = sc.string[0..(sc.pos-1)] # for authentication by _decrypt format = CODE_2_FORMAT[f] fsize = sc.scan(/[0-9]+/) or raise BAD_STR, "bad fsize" fsize = fsize.to_i _ = sc.scan(/_/) or raise BAD_STR, "missing _" formatted = sc.peek(fsize) begin sc.pos += fsize rescue RangeError => ex raise CantTouchThisStringError, "#{ex.class}: #{ex.}" end _ = sc.scan(/_ok$/) or raise BAD_STR, "bad ok" encrypted = _deformat(format,formatted) plaintext = _decrypt(encryption_key,encrypted,header) sp = StringScanner.new(plaintext) r = sp.scan(/[NCS]/) or raise BAD_STR, "bad redundancy" c = sp.scan(/[N4ZBM]/) or raise BAD_STR, "bad compression" scheck = sp.scan(/[a-f0-9]+/) or raise BAD_STR, "bad scheck" _ = sp.scan(/_/) or raise BAD_STR, "missing _" compressed = sp.rest redundancy = CODE_2_REDUNDANCY[r] || r compression = CODE_2_COMPRESSION[c] || c original = _decompress(compression,compressed) scheck_re = _check(redundancy,original) if scheck != scheck_re raise( CantTouchThisStringError, "scheck #{scheck} vs #{scheck_re} in #{sc.string}" ) end original end |
._wrap(str, opts = {}) ⇒ Object
Wraps any string into a OAK string.
Is inverted by unwrap(). For all str, unwrap(wrap(str)) == str.
557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 |
# File 'lib/oak.rb', line 557 def self._wrap(str,opts={}) redundancy = (opts[:redundancy] || :crc32).to_s compression = (opts[:compression] || :none).to_s force = (opts[:force] || false) format = (opts[:format] || :base64).to_s key_chain = opts[:key_chain] key = opts[:key] debug_iv = opts[:debug_iv] if key_chain && !key_chain.is_a?(KeyChain) raise ArgumentError, "bad key_chain #{key_chain}" end if debug_iv && !debug_iv.is_a?(String) raise ArgumentError, "bad debug_iv #{debug_iv}" end if debug_iv && ENCRYPTION_ALGO_IV_BYTES != debug_iv.size raise ArgumentError, "bad debug_iv #{debug_iv}" end if key && !key_chain raise ArgumentError, "key #{key} without key_chain" end if key && !key_chain.keys[key] keys = key_chain.keys raise ArgumentError, "key not found in #{keys}: #{key}" end encryption_key = key ? key_chain.keys[key] : nil str = str.b # dupe to Encoding::ASCII_8BIT if encryption_key || opts[:force_oak_4] _wrap_oak_4( str, redundancy, compression, force, format, key, encryption_key, debug_iv ) else _wrap_oak_3( str, redundancy, compression, force, format ) end end |
._wrap_oak_3(str, redundancy, compression, force, format) ⇒ Object
605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 |
# File 'lib/oak.rb', line 605 def self._wrap_oak_3( str, redundancy, compression, force, format ) source_redundancy = _check(redundancy,str) compressed, compression = _compress(compression,force,str) formatted = _format(format,compressed) output = 'oak_3' # format id+ver output << REDUNDANCY_2_CODE[redundancy] # redundancy output << COMPRESSION_2_CODE[compression] # compression output << FORMAT_2_CODE[format] # format output << '_' output << source_redundancy # source check output << '_' output << '%d' % formatted.size # formatted size output << '_' output << formatted # payload output << '_' output << 'ok' # terminator output.force_encoding(Encoding::ASCII_8BIT) end |
._wrap_oak_4(str, redundancy, compression, force, format, key, encryption_key, debug_iv) ⇒ Object
630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 |
# File 'lib/oak.rb', line 630 def self._wrap_oak_4( str, redundancy, compression, force, format, key, encryption_key, debug_iv ) header = 'oak_4' # format id+ver if key header << key # key name end header << '_' header << FORMAT_2_CODE[format] # format compressed, compression = _compress(compression,force,str) plaintext = '' plaintext << REDUNDANCY_2_CODE[redundancy] # redundancy plaintext << COMPRESSION_2_CODE[compression] # compression plaintext << _check(redundancy,str) # source check plaintext << '_' plaintext << compressed encrypted = _encrypt( encryption_key, plaintext, header, debug_iv ) formatted = _format(format,encrypted) output = header output << '%d' % formatted.size # formatted size output << '_' output << formatted # payload output << '_' output << 'ok' # terminator output.force_encoding(Encoding::ASCII_8BIT) end |
.decode(str, opts = {}) ⇒ Object
Decodes suitable OAK strings into objects.
Inverts encode().
decrypt encrypted OAK strings, or nil for none.
225 226 227 228 229 230 231 |
# File 'lib/oak.rb', line 225 def self.decode(str,opts={}) if !str.is_a?(String) raise ArgumentError, "str not a String" end ser = _unwrap(str,opts) _deserialize(ser) end |
.encode(obj, opts = {}) ⇒ Object
Encodes suitable objects string into OAK strings.
Is inverted by decode(). For all obj, if encode(obj) does not raise an exception, decode(encode(obj)) == obj.
WARNING: Use of debug_iv jeopardizes the security of all messages ever encrypted with that key! Never use debug_iv in production!
207 208 209 210 |
# File 'lib/oak.rb', line 207 def self.encode(obj,opts={}) ser = _serialize(obj) _wrap(ser,opts) end |
.encryption_algo ⇒ Object
Get a new instance of OpenSSL::Cipher for our algorithm.
54 55 56 |
# File 'lib/oak.rb', line 54 def self.encryption_algo OpenSSL::Cipher.new(ENCRYPTION_ALGO_NAME) end |
.parse_env_chain(env, name) ⇒ Object
Parses a KeyChain object and keys from an ENV-like object.
E.g. if the ENV contains:
FOO_KEYS=a,b
FOO_KEY_a=#{OAK.encode(<binary key>)}
FOO_KEY_b=#{OAK.encode(<binary key>)}
…then the call OAK.parse_key_chain(ENV,‘FOO’) will return a new OAK::KeyChain with two OAK::Keys, ‘a’ and ‘b’.
This self-referential (but not recursive!) use of OAK to encode the key and iv is to avoid the problems with binary strings in ENV variables, ‘heroku config:set’ command line arguments, etc.
155 156 157 158 159 160 161 162 |
# File 'lib/oak.rb', line 155 def self.parse_env_chain(env,name) key_names = (env["#{name}_KEYS"] || '').gsub(/^[, ]*/,'').split(/[ ,]+/) keys = key_names.map do |key_name| key = OAK.decode(env["#{name}_KEY_#{key_name}"] || '') [ key_name, Key.new(key) ] end.to_h KeyChain.new(keys) end |
.random_iv ⇒ Object
Generate a new random initialization vector appropriate for the OAK_4 encryption algorithm.
68 69 70 |
# File 'lib/oak.rb', line 68 def self.random_iv encryption_algo.random_iv end |
.random_key ⇒ Object
Generate a new random key appropriate for the OAK_4 encryption algorithm.
61 62 63 |
# File 'lib/oak.rb', line 61 def self.random_key encryption_algo.random_key end |