Class: OStatus::Salmon

Inherits:
Object
  • Object
show all
Defined in:
lib/ostatus/salmon.rb

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(entry, signature = nil, plaintext = nil) ⇒ Salmon

Create a Salmon instance for a particular OStatus::Entry



10
11
12
13
14
# File 'lib/ostatus/salmon.rb', line 10

def initialize entry, signature = nil, plaintext = nil
  @entry = entry
  @signature = signature
  @plaintext = plaintext
end

Instance Attribute Details

#entryObject

Returns the value of attribute entry.



7
8
9
# File 'lib/ostatus/salmon.rb', line 7

def entry
  @entry
end

Class Method Details

.from_follow(user_author, followed_author) ⇒ Object

Creates an entry for following a particular Author.



17
18
19
20
21
22
23
24
25
26
27
28
# File 'lib/ostatus/salmon.rb', line 17

def Salmon.from_follow(user_author, followed_author)
  entry = OStatus::Entry.new(
    :author => user_author,
    :title => "Now following #{followed_author.name}",
    :content => Atom::Content::Html.new("Now following #{followed_author.name}")
  )

  entry.activity.verb = :follow
  entry.activity_object = followed_author

  OStatus::Salmon.new(entry)
end

.from_unfollow(user_author, followed_author) ⇒ Object

Creates an entry for unfollowing a particular Author.



31
32
33
34
35
36
37
38
39
40
41
42
# File 'lib/ostatus/salmon.rb', line 31

def Salmon.from_unfollow(user_author, followed_author)
  entry = OStatus::Entry.new(
    :author => user_author,
    :title => "Stopped following #{followed_author.name}",
    :content => Atom::Content::Html.new("Stopped following #{followed_author.name}")
  )

  entry.activity_verb = "http://ostatus.org/schema/1.0/unfollow"
  entry.activity_object = followed_author

  OStatus::Salmon.new(entry)
end

.from_xml(source) ⇒ Object

Will pull a OStatus::Entry from a magic envelope described by the xml.



45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# File 'lib/ostatus/salmon.rb', line 45

def Salmon.from_xml source
  if source.is_a?(String)
    if source.length == 0
      return nil
    end

    source = XML::Document.string(source,
                                  :options => XML::Parser::Options::NOENT)
  end

  # Retrieve the envelope
  envelope = source.find('/me:env',
                      'me:http://salmon-protocol.org/ns/magic-env').first

  if envelope.nil?
    return nil
  end

  data = envelope.find('me:data',
                       'me:http://salmon-protocol.org/ns/magic-env').first
  if data.nil?
    return nil
  end

  data_type = data.attributes["type"]
  if data_type.nil?
    data_type = 'application/atom+xml'
    armored_data_type = ''
  else
    armored_data_type = Base64::urlsafe_encode64(data_type)
  end

  encoding = envelope.find('me:encoding',
                           'me:http://salmon-protocol.org/ns/magic-env').first

  algorithm = envelope.find(
                      'me:alg',
                      'me:http://salmon-protocol.org/ns/magic-env').first

  signature = source.find('me:sig',
                       'me:http://salmon-protocol.org/ns/magic-env').first

  # Parse fields

  if signature.nil?
    # Well, if we cannot verify, we don't accept
    return nil
  else
    # XXX: Handle key_id attribute
    signature = signature.content
    signature = Base64::urlsafe_decode64(signature)
  end

  if encoding.nil?
    # When the encoding is omitted, use base64url
    # Cite: Magic Envelope Draft Spec Section 3.3
    armored_encoding = ''
    encoding = 'base64url'
  else
    armored_encoding = Base64::urlsafe_encode64(encoding.content)
    encoding = encoding.content.downcase
  end

  if algorithm.nil?
    # When algorithm is omitted, use 'RSA-SHA256'
    # Cite: Magic Envelope Draft Spec Section 3.3
    armored_algorithm = ''
    algorithm = 'rsa-sha256'
  else
    armored_algorithm = Base64::urlsafe_encode64(algorithm.content)
    algorithm = algorithm.content.downcase
  end

  # Retrieve and decode data payload

  data = data.content
  armored_data = data

  case encoding
  when 'base64url'
    data = Base64::urlsafe_decode64(data)
  else
    # Unsupported data encoding
    return nil
  end

  # Signature plaintext
  plaintext = "#{armored_data}.#{armored_data_type}.#{armored_encoding}.#{armored_algorithm}"

  # Interpret data payload
  payload = XML::Reader.string(data)
  Salmon.new OStatus::Entry.new(payload), signature, plaintext
end

Instance Method Details

#sign(message, key) ⇒ Object



197
198
199
200
201
202
203
204
# File 'lib/ostatus/salmon.rb', line 197

def sign message, key
  @plaintext = message

  modulus_byte_count = key.private_key.modulus.size

  @signature = signature(modulus_byte_count)
  @signature = key.decrypt(@signature)
end

#signature(modulus_byte_length) ⇒ Object

Return the EMSA string for this Salmon instance given the size of the public key modulus.



183
184
185
186
187
188
189
190
191
192
193
194
195
# File 'lib/ostatus/salmon.rb', line 183

def signature modulus_byte_length
  plaintext = Digest::SHA2.new(256).digest(@plaintext)

  prefix = "\x30\x31\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x01\x05\x00\x04\x20"
  padding_count = modulus_byte_length - prefix.bytes.count - plaintext.bytes.count - 3

  padding = ""
  padding_count.times do
    padding = padding + "\xff"
  end

  "\x00\x01#{padding}\x00#{prefix}#{plaintext}"
end

#to_xml(key) ⇒ Object

Generate the xml for this Salmon notice and sign with the given private key.



141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
# File 'lib/ostatus/salmon.rb', line 141

def to_xml key
  # Generate magic envelope
  magic_envelope = XML::Document.new

  magic_envelope.root = XML::Node.new 'env'

  me_ns = XML::Namespace.new(magic_envelope.root,
               'me', 'http://salmon-protocol.org/ns/magic-env')

  magic_envelope.root.namespaces.namespace = me_ns

  # Armored Data <me:data>
  data = @entry.to_xml
  @plaintext = data
  data_armored = Base64::urlsafe_encode64(data)
  elem = XML::Node.new 'data', data_armored, me_ns
  elem.attributes['type'] = 'application/atom+xml'
  data_type_armored = 'YXBwbGljYXRpb24vYXRvbSt4bWw='
  magic_envelope.root << elem

  # Encoding <me:encoding>
  magic_envelope.root << XML::Node.new('encoding', 'base64url', me_ns)
  encoding_armored = 'YmFzZTY0dXJs'

  # Signing Algorithm <me:alg>
  magic_envelope.root << XML::Node.new('alg', 'RSA-SHA256', me_ns)
  algorithm_armored = 'UlNBLVNIQTI1Ng=='

  # Signature <me:sig>
  plaintext = "#{data_armored}.#{data_type_armored}.#{encoding_armored}.#{algorithm_armored}"

  # Assign @signature to the signature generated from the plaintext
  sign(plaintext, key)

  signature_armored = Base64::urlsafe_encode64(@signature)
  magic_envelope.root << XML::Node.new('sig', signature_armored, me_ns)

  magic_envelope.to_s :indent => true, :encoding => XML::Encoding::UTF_8
end

#verified?(key) ⇒ Boolean

Use RSA to verify the signature key - RSA::KeyPair with the public key to use

Returns:

  • (Boolean)


208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
# File 'lib/ostatus/salmon.rb', line 208

def verified? key
  # RSA encryption is needed to compare the signatures

  # Get signature to check
  emsa = self.signature key.public_key.modulus.size

  # Get signature in payload
  emsa_signature = key.encrypt(@signature)

  # RSA gem drops leading 0s since it does math upon an Integer
  # As a workaround, I check for what I expect the second byte to be (\x01)
  # This workaround will also handle seeing a \x00 first if the RSA gem is
  # fixed.
  if emsa_signature.getbyte(0) == 1
    emsa_signature = "\x00#{emsa_signature}"
  end

  # Does the signature match?
  # Return the result.
  emsa_signature == emsa
end