diary-ruby 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,95 @@
1
+ # -*- encoding: utf-8 -*-
2
+ #
3
+ # Create an encrypted bundled string version of a message, given a particular
4
+ # passphrase.
5
+ #
6
+ # Decrypt a bundled document given a particular passphrase.
7
+ #
8
+ # NOTE: the encrypted bundle chooses a random initialization vector
9
+ # and salt and includes them in the bundle in plain text alongside the
10
+ # encrypted message.
11
+ #
12
+
13
+ require 'openssl'
14
+ require 'base64'
15
+
16
+ module Encryptor
17
+ class Error < StandardError
18
+ end
19
+
20
+ def self.encrypt(msg, pwd)
21
+ cipher = OpenSSL::Cipher.new 'AES-128-CBC'
22
+ cipher.encrypt
23
+
24
+ # random salt
25
+ salt = OpenSSL::Random.random_bytes(16)
26
+
27
+ # random initialization vector
28
+ iv = cipher.random_iv
29
+
30
+ iter = 20000
31
+ key_len = cipher.key_len
32
+ digest = OpenSSL::Digest::SHA256.new
33
+
34
+ key = OpenSSL::PKCS5.pbkdf2_hmac(pwd, salt, iter, key_len, digest)
35
+ cipher.key = key
36
+
37
+ # Now encrypt the data:
38
+ encrypted = cipher.update(msg)
39
+ encrypted << cipher.final
40
+
41
+ # And encode final format
42
+ wrap(iv, salt, encrypted)
43
+ end
44
+
45
+ def self.decrypt(document, pwd)
46
+ iv, salt, encrypted = unwrap(document)
47
+
48
+ Diary.debug "DECRYPT WITH"
49
+ Diary.debug " iv #{ Base64.encode64(iv) }"
50
+ Diary.debug " salt #{ Base64.encode64(salt) }"
51
+ Diary.debug " msg #{ Base64.encode64(encrypted) }"
52
+
53
+ ## Decrypt
54
+ cipher = OpenSSL::Cipher.new 'AES-128-CBC'
55
+ cipher.decrypt
56
+ cipher.iv = iv
57
+
58
+ salt = salt
59
+ iter = 20000
60
+ key_len = cipher.key_len
61
+ digest = OpenSSL::Digest::SHA256.new
62
+
63
+ key = OpenSSL::PKCS5.pbkdf2_hmac(pwd, salt, iter, key_len, digest)
64
+ cipher.key = key
65
+
66
+ decrypted = cipher.update(encrypted)
67
+ decrypted << cipher.final
68
+ end
69
+
70
+ def self.wrap(iv, salt, encrypted)
71
+ [
72
+ Base64.encode64(iv),
73
+ Base64.encode64(salt),
74
+ Base64.encode64(encrypted)
75
+ ].join('|')
76
+ end
77
+
78
+ def self.unwrap(document)
79
+ if document.is_a?(File)
80
+ document = document.read
81
+ end
82
+
83
+ if document.count('|') != 2
84
+ raise Encryptor::Error.new("Document is not a vaild encrypted store.")
85
+ end
86
+
87
+ iv64, salt64, encrypted64 = document.split('|')
88
+
89
+ iv = Base64.decode64(iv64.to_s.strip)
90
+ salt = Base64.decode64(salt64.to_s.strip)
91
+ encrypted = Base64.decode64(encrypted64.to_s.strip)
92
+
93
+ [iv, salt, encrypted]
94
+ end
95
+ end
@@ -0,0 +1,86 @@
1
+
2
+ require 'pstore'
3
+ require 'diary-ruby/ext/encryptor'
4
+
5
+ #
6
+ # Wrap PStore, combine with OpenSSL::Cipher to secure store on disk with a
7
+ # given passphrase
8
+ #
9
+ # SecurePStore
10
+ #
11
+ # Useable exactly like PStore except for initialization.
12
+ #
13
+ # With PStore:
14
+ #
15
+ # wiki = PStore.new("wiki_pages.pstore")
16
+ # wiki.transaction do # begin transaction; do all of this or none of it
17
+ # # store page...
18
+ # wiki[home_page.page_name] = home_page
19
+ # # ensure that an index has been created...
20
+ # wiki[:wiki_index] ||= Array.new
21
+ # # update wiki index...
22
+ # wiki[:wiki_index].push(*home_page.wiki_page_references)
23
+ # end # commit changes to wiki data store file
24
+ #
25
+ # With SecurePStore:
26
+ #
27
+ # wiki = SecurePStore.new("wiki_pages.pstore", passphrase: 'do it this way instead')
28
+ # wiki.transaction do # begin transaction; do all of this or none of it
29
+ # # store page...
30
+ # wiki[home_page.page_name] = home_page
31
+ # # ensure that an index has been created...
32
+ # wiki[:wiki_index] ||= Array.new
33
+ # # update wiki index...
34
+ # wiki[:wiki_index].push(*home_page.wiki_page_references)
35
+ # end # commit changes to wiki data store file
36
+ #
37
+ # Simple!
38
+ #
39
+ class SecurePStore < PStore
40
+ # :call-seq:
41
+ # initialize( file_name, secure_opts = {} )
42
+ #
43
+ # Creates a new SecureStore object, which will store data in +file_name+.
44
+ # If the file does not already exist, it will be created.
45
+ #
46
+ # Options passed in through +secure_opts+ will be used behind the scenes
47
+ # when writing the encrypted file to disk.
48
+ def initialize file_name, secure_opts = {}
49
+ @opt = secure_opts
50
+ super
51
+ end
52
+
53
+ # Override PStore's private low-level storage methods, similar to YAML::Store
54
+ #
55
+ def dump(table) # :nodoc:
56
+ marshalled = Marshal::dump(table)
57
+ # return encrypted
58
+ Encryptor.encrypt(marshalled, @opt[:passphrase])
59
+ end
60
+
61
+ def load(content) # :nodoc:
62
+ begin
63
+ dcontent = Encryptor.decrypt(content, @opt[:passphrase])
64
+ Marshal::load(dcontent)
65
+ rescue OpenSSL::Cipher::CipherError => ex
66
+ raise PStore::Error.new("Failed to decrypt stored data: #{ ex.message }")
67
+ end
68
+ end
69
+
70
+ def marshal_dump_supports_canonical_option?
71
+ false
72
+ end
73
+
74
+ def empty_marshal_data
75
+ @empty_marshal_data ||= begin
76
+ m = Marshal.dump({})
77
+ Encryptor.encrypt(m, @opt[:passphrase])
78
+ end
79
+ end
80
+
81
+ def empty_marshal_checksum
82
+ @empty_marshal_checksum ||= begin
83
+ Digest::MD5.digest(empty_marshal_data)
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,68 @@
1
+ # Parse a plaintext diary entry into a Diary::Entry object
2
+
3
+ module Diary
4
+ class Parser
5
+ def self.parse(infile)
6
+ header = []
7
+ body = []
8
+ in_header = true
9
+ split_match = /^---+$/
10
+
11
+ Diary.debug("PARSE #{ infile.size } BYTES")
12
+
13
+ infile.lines.each do |line|
14
+ if in_header
15
+ if split_match =~ line
16
+ in_header = false
17
+ next
18
+ end
19
+
20
+ # check for line
21
+ header << line
22
+ else
23
+ body << line
24
+ end
25
+ end
26
+
27
+ metadata = {}
28
+
29
+ key_match = /^([A-Za-z_-]+):? (.+)$/
30
+ header.each do |h_line|
31
+ if key_match =~ h_line
32
+ key = $1.strip.downcase
33
+ val = $2.strip
34
+
35
+ if /tags/i =~ key
36
+ val = val.split(',').map {|v| v.strip}
37
+ end
38
+
39
+ metadata[key] = val
40
+ end
41
+ end
42
+
43
+ key = Entry.keygen(metadata['day'], metadata['time'])
44
+ Diary.debug "KEY #{ key }"
45
+ Diary.debug "METADATA #{ metadata.inspect }"
46
+ Diary.debug "BODY #{ body.join(" ") }"
47
+
48
+ return Entry.new(
49
+ nil,
50
+ day: metadata['day'],
51
+ time: metadata['time'],
52
+ tags: metadata['tags'],
53
+ text: body.join("\n").strip,
54
+ title: metadata['title'],
55
+ key: key,
56
+ )
57
+ end
58
+
59
+ def self.parse_file(file)
60
+ # read
61
+ file.seek(0)
62
+ contents = file.read
63
+
64
+ # now parse
65
+ self.parse(contents)
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,57 @@
1
+ // alert('hello world!')
2
+
3
+ // vanilla js AJAX
4
+ /*
5
+ var r = new XMLHttpRequest();
6
+ r.open("POST", "path/to/api", true);
7
+ r.onreadystatechange = function () {
8
+ if (r.readyState != 4 || r.status != 200) return;
9
+ alert("Success: " + r.responseText);
10
+ };
11
+ r.send("banana=yellow");
12
+ */
13
+
14
+ var each = function (el, func) {
15
+ if (el instanceof NodeList) {
16
+ var forEach = Array.prototype.forEach
17
+ forEach.call(el, func)
18
+ } else {
19
+ func(el)
20
+ }
21
+ return el
22
+ }
23
+
24
+ // get NodeList of elements
25
+ var $$ = function (selector) {
26
+ return document.querySelectorAll(selector)
27
+ }
28
+
29
+ // get single element
30
+ var $ = function (selector) {
31
+ return document.querySelector(selector)
32
+ }
33
+
34
+ var zp = function (n) {
35
+ n = parseInt(n)
36
+ if (n < 10 && n >= 0) {
37
+ return "0" + n
38
+ } else {
39
+ return n
40
+ }
41
+ }
42
+
43
+ var strftime_F = function () {
44
+ var now = new Date()
45
+ return now.getFullYear() + "-" + zp(now.getMonth() + 1) + "-" + zp(now.getDate())
46
+ }
47
+
48
+ var strftime_T = function () {
49
+ var now = new Date()
50
+ return zp(now.getHours()) + ":" + zp(now.getMinutes()) + ":" + zp(now.getSeconds())
51
+ }
52
+
53
+ window.onload = function () {
54
+ $('input.day').value = strftime_F()
55
+ $('input.time').value = strftime_T()
56
+ }
57
+
@@ -0,0 +1,33 @@
1
+ .wrapper {
2
+ width: 960px;
3
+ margin: 0 auto;
4
+ }
5
+
6
+ form {
7
+ }
8
+
9
+ form input[type=text] {
10
+ border: 0;
11
+ border-bottom: 1px solid black;
12
+ }
13
+
14
+ form input.tags {
15
+ width: 300px;
16
+ }
17
+
18
+ form label {
19
+ display: block;
20
+ margin-bottom: 8px;
21
+ }
22
+
23
+ form label span {
24
+ display: inline-block;
25
+ width: 50px;
26
+ }
27
+
28
+ form textarea {
29
+ display: block;
30
+ width: 600px;
31
+ height: 300px;
32
+ margin-top: 8px;
33
+ }
@@ -0,0 +1,93 @@
1
+ require 'sinatra'
2
+ require 'json'
3
+ require 'rdiscount'
4
+ require 'tilt/erb'
5
+
6
+ module Diary
7
+ class Server < Sinatra::Base
8
+ configure :production, :development do
9
+ enable :logging
10
+ end
11
+
12
+ def self.store=(val)
13
+ @store = val
14
+ end
15
+
16
+ def self.store
17
+ @store
18
+ end
19
+
20
+ def store
21
+ self.class.store
22
+ end
23
+
24
+ get '/' do
25
+ keys = store.read(:entries)
26
+
27
+ if keys.nil?
28
+ store.write do |db|
29
+ db[:entries] = []
30
+ end
31
+
32
+ keys = []
33
+ end
34
+
35
+ @entries = keys.uniq.map {|entry_key|
36
+ entry = store.read(entry_key)
37
+
38
+ if entry
39
+ logger.debug "LOAD #{ entry }"
40
+ Entry.from_store(entry)
41
+ else
42
+ nil
43
+ end
44
+ }.compact
45
+
46
+ logger.info "returning keys: #{ keys }"
47
+ logger.info "returning entries: #{ @entries }"
48
+
49
+ erb :index
50
+ end
51
+
52
+ get '/entry/:key' do
53
+ content_type :json
54
+
55
+ key = params[:key]
56
+ entry_hash = store.read(key)
57
+ entry = Entry.from_store(entry_hash)
58
+
59
+ content = RDiscount.new entry.text
60
+ entry_hash[:formatted] = content.to_html
61
+
62
+ if entry
63
+ entry_hash.to_json
64
+ else
65
+ {}
66
+ end
67
+ end
68
+
69
+ # create
70
+ post '/entries' do
71
+ logger.info "ENTRY"
72
+
73
+ begin
74
+ tags = params[:tags].split(',').map(&:strip)
75
+ rescue
76
+ tags = []
77
+ end
78
+
79
+ entry = Entry.new(
80
+ nil,
81
+ day: params[:day],
82
+ time: params[:time],
83
+ tags: tags,
84
+ text: params[:text],
85
+ )
86
+
87
+ store.write_entry(entry)
88
+
89
+ redirect to('/')
90
+ end
91
+ end
92
+ end
93
+
@@ -0,0 +1,61 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <link href='style.css' rel='stylesheet' type='text/css' />
5
+ <script src='script.js'></script>
6
+ </head>
7
+
8
+ <body>
9
+ <div class='wrapper'>
10
+ <h1>Existing Entries</h1>
11
+ <% if @entries.size == 0 %>
12
+ <p>Nothing to see here...</p>
13
+ <% else %>
14
+ <% @entries.each do |entry| %>
15
+ <div class='entry'>
16
+ <p>
17
+ <strong><%= entry.day %> <%= entry.time %></strong>
18
+ <a href='/entry/<%= entry.key %>'>json</a>
19
+ </p>
20
+ <div class='entry--text'>
21
+ <%= entry.formatted_text %>
22
+ </div>
23
+ </div>
24
+ <% end %>
25
+ <% end %>
26
+
27
+ <hr>
28
+
29
+ <h3>New Entry</h3>
30
+ <form action='/entries' method='POST'>
31
+ <label>
32
+ <span>Day</span>
33
+ <input class='day' name='day' type='text' />
34
+ </label>
35
+
36
+ <label>
37
+ <span>Time</span>
38
+ <input class='time' name='time' type='text' />
39
+ </label>
40
+
41
+ <label>
42
+ <span>Tags</span>
43
+ <input type='text' class='tags' name='tags' />
44
+ </label>
45
+
46
+ <label>
47
+ <span>Text</span>
48
+ <textarea class='text' name='text'></textarea>
49
+ </label>
50
+
51
+ <input type='submit' value='commit' />
52
+ </form>
53
+ </div>
54
+ </body>
55
+ </html>
56
+
57
+
58
+
59
+
60
+
61
+