Skip to content
GitLab
Projects
Groups
Snippets
Help
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Federation Library
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Locked Files
Issues
6
Issues
6
List
Boards
Labels
Service Desk
Milestones
Merge Requests
1
Merge Requests
1
Requirements
Requirements
List
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Security & Compliance
Security & Compliance
Dependency List
License Compliance
Operations
Operations
Environments
Packages & Registries
Packages & Registries
Container Registry
Analytics
Analytics
CI / CD
Code Review
Insights
Issue
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
ganggo
Federation Library
Commits
d41b4df7
Commit
d41b4df7
authored
May 10, 2017
by
zauberstuhl
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Clean up EncryptedMagicEnvelope
parent
43eb17fb
Changes
6
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
89 additions
and
199 deletions
+89
-199
aes.go
aes.go
+5
-11
encryption.go
encryption.go
+10
-6
entities.go
entities.go
+0
-82
header.go
header.go
+30
-60
http_client.go
http_client.go
+13
-11
magic.go
magic.go
+31
-29
No files found.
aes.go
View file @
d41b4df7
...
...
@@ -19,10 +19,10 @@ package federation
import
(
"bytes"
"io"
"encoding/base64"
"crypto/aes"
"crypto/cipher"
"io"
"crypto/rand"
)
...
...
@@ -63,15 +63,9 @@ func (a *Aes) Encrypt(data []byte) error {
// CBC mode works on blocks so plaintexts may need to be padded to the
// next whole block. For an example of such padding, see
// https://tools.ietf.org/html/rfc5246#section-6.2.3.2.
if
len
(
data
)
%
aes
.
BlockSize
!=
0
{
paddingLen
:=
aes
.
BlockSize
-
(
len
(
data
)
%
aes
.
BlockSize
)
paddingText
:=
bytes
.
Repeat
([]
byte
{
byte
(
paddingLen
)},
paddingLen
)
//for i := 0; i < paddingLen; i++ {
// data = append(data, 0x20)
//}
data
=
append
(
data
,
paddingText
...
)
}
padding
:=
aes
.
BlockSize
-
len
(
data
)
%
aes
.
BlockSize
padtext
:=
bytes
.
Repeat
([]
byte
{
byte
(
padding
)},
padding
)
data
=
append
(
data
,
padtext
...
)
key
,
err
:=
base64
.
StdEncoding
.
DecodeString
(
a
.
Key
)
if
err
!=
nil
{
...
...
@@ -91,7 +85,7 @@ func (a *Aes) Encrypt(data []byte) error {
}
mode
:=
cipher
.
NewCBCEncrypter
(
block
,
iv
)
mode
.
CryptBlocks
(
ciphertext
[
:
]
,
data
)
mode
.
CryptBlocks
(
ciphertext
,
data
)
a
.
Data
=
base64
.
StdEncoding
.
EncodeToString
(
ciphertext
)
return
nil
...
...
encryption.go
View file @
d41b4df7
...
...
@@ -100,6 +100,8 @@ func (request *DiasporaUnmarshal) VerifySignature(serialized []byte) error {
}
func
AuthorSignature
(
data
interface
{},
privKey
string
)
(
string
,
error
)
{
// XXX the order should be tracked in the database
// otherwise this can break stuff very quickly
var
text
string
switch
entity
:=
data
.
(
type
)
{
case
*
EntityComment
:
...
...
@@ -113,24 +115,26 @@ func AuthorSignature(data interface{}, privKey string) (string, error) {
// positive guid parent_guid parent_type author
text
=
positive
+
";"
+
entity
.
Guid
+
";"
+
entity
.
ParentGuid
+
";"
+
entity
.
TargetType
+
";"
+
entity
.
DiasporaHandle
default
:
panic
(
"Not supported interface type for AuthorSignature!"
)
}
return
Sign
(
text
,
privKey
)
}
func
(
d
*
PrivateEnv
Marshal
)
Sign
(
privKey
string
)
(
err
error
)
{
func
(
envelope
*
MagicEnvelope
Marshal
)
Sign
(
privKey
string
)
(
err
error
)
{
type64
:=
base64
.
StdEncoding
.
EncodeToString
(
[]
byte
(
d
.
Data
.
Type
),
[]
byte
(
envelope
.
Data
.
Type
),
)
encoding64
:=
base64
.
StdEncoding
.
EncodeToString
(
[]
byte
(
d
.
Encoding
),
[]
byte
(
envelope
.
Encoding
),
)
alg64
:=
base64
.
StdEncoding
.
EncodeToString
(
[]
byte
(
d
.
Alg
),
[]
byte
(
envelope
.
Alg
),
)
text
:=
d
.
Data
.
Data
+
"."
+
type64
+
text
:=
envelope
.
Data
.
Data
+
"."
+
type64
+
"."
+
encoding64
+
"."
+
alg64
(
*
d
)
.
Sig
.
Sig
,
err
=
Sign
(
text
,
privKey
)
(
*
envelope
)
.
Sig
.
Sig
,
err
=
Sign
(
text
,
privKey
)
return
}
...
...
entities.go
View file @
d41b4df7
...
...
@@ -19,88 +19,6 @@ package federation
import
"encoding/xml"
/* Header
<?xml version="1.0" encoding="UTF-8"?>
<diaspora xmlns="https://joindiaspora.com/protocol" xmlns:me="http://salmon-protocol.org/ns/magic-env">
<header>
<author_id>dia@192.168.0.173:3000</author_id>
</header>
*/
type
Header
struct
{
XMLName
xml
.
Name
`xml:"header"`
AuthorId
string
`xml:"author_id"`
}
/* Encrypted Header
<?xml version="1.0" encoding="UTF-8"?>
<decrypted_header>
<iv>...</iv>
<aes_key>...</aes_key>
<author_id>one@two.tld</author_id>
</decrypted_header>
*/
// XXX legacy?
type
XmlDecryptedHeader
struct
{
XMLName
xml
.
Name
`xml:"decrypted_header"`
Iv
string
`xml:"iv"`
AesKey
string
`xml:"aes_key"`
AuthorId
string
`xml:"author_id"`
}
type
JsonEnvHeader
struct
{
AesKey
string
`json:"aes_key"`
Ciphertext
string
`json:"ciphertext"`
}
// Private Request
//
// <?xml version="1.0" encoding="UTF-8"?>
// <diaspora xmlns="https://joindiaspora.com/protocol" xmlns:me="http://salmon-protocol.org/ns/magic-env">
// <encrypted_header>eyJhZXNfa2V5IjoiVTFGbVZ5TE5CT0pyOWViZVpiMUxnSGQzMlJwMzNMa1ViZVRnY3BEaDluZHAyT2cyMUZNeHh2Mm9RUXp0eWxLVjZsOEkzR0wvQlEzcEQxbGNYbjFGQWlEVTBmTHJwRUZwUEJNc1AwT3MvdmhEZ3I3MDJvaWNkMUxFN0ZMZ3ZVc1VKRGxGOHdvTVZUaGtnTmNVWGlCNUZpajcvb1J4ZFZ1QlJxbVpCQ0VXLys4PSIsImNpcGhlcnRleHQiOiJveWhWVjR5bXJoTGxYRVU1WVcxQWdpK29sTkxRSDlvR2NPQnVBOG00a25FZ09GZnJoNzEwUi9XQloyQ1I3WVlMeEhuOEcvUWFyU1UraDFka0VHMGFhcS9FQ1RWQjFHOXNkT2lQMnZSRTRkY1JOclNqb3RINXBsV3F4QVBvNlBpRkdwNUJZcURoYnB2RUNYNFNpM3BTN2hyOTdyWUs3NTFyYnYvRjNwRERJNG13d2NoUFA3S21nN21yVXBCZTBEeXRqQk5xK0k3S2lxSVQrRmVTUUMxc0pQSmd4TVAxck1VcDB6cURpT2ZlR3U2RGVnVG5MRnd5WHM3WEhGQW1FN29iSlZJY2NxS3czNjNWcnBrWXE0N1BDRWc3R3p1bFVlOXA5eFhDN3dVRmNOak91RTVQV1NXU1h3cWM0K2EybHFuOCJ9</encrypted_header>
// <me:env>
// <me:data type="application/xml">WXRPc09JakFleHh0dW5rQytRallnSEg0a08zOGd3ZVQyVVg4NkppVVZzVlJlMFFHbExhSjBtbzBnRUVGSnQyc2pqRG5OOUFETk1nakJ6MThGVnJsRWNEcjBqUDJ2emZPRjlCVHVLNXlvRjZqQ3NIRlh5USt4L3RlMnUxN0xCSUlxSDk4OGNhL1BVLzE2NEhBL0plQU1sMEZCOFBnTTFzYW9qUUJaV3V1RW95Y25ONWVZTm9wRi9adElXU3AxOFVzL05RWG13cktiWVVYYXJpNitOSWo1RWVOdTJrYnJnN2FoWUdhRFJKMVFtcEdhL0pMNmVvMEM1ZHFkS2s1amgrNFU0dnA0ZVY5bkRPK0ZGSkhpd3pDWm84NWJkdUR4T0NRRURpa2QxeGE1OWl4NkZWMTYzQ0MvMXBqdmhTa3cvTjNOTGdHOUZjWExQZDRwdTYzQ2pkS1E4QlE2MTYrMEFGREV0bjF0RldqYU9DV0VXOW5ORzQrTmNVZ0ZKamM0dzZxU3Ruc00rSFdzS0hOTnpRZElRc1pXT3c2bzJUK3ZCOUZmeUY3T3NTNHhwYnF0OW5MZFM1Z0U1M1lFb3FXVmRzK3E1a2xYV3dXdjlBeHRGdlhIVWxyNEE9PQ==</me:data>
// <me:encoding>base64url</me:encoding>
// <me:alg>RSA-SHA256</me:alg>
// <me:sig>sJPN--TJ9IqVwhT_j9mNGrLF4yQvwXUQKo24cPLi5FVXl-tVpyEOxrUI1gwRJ5j5UkkqNJO8mLph2ravlxqt7PNhS9YAOTuo46nXWXyOJjP_ESxq3DaMrYqQt57PDnM29x5yQ0QATbSAs6XneHtxmVKzwKgi4ZpdQ3THFj_iWwac0BI3Or1okt9wLxxl3LTLO9vwfIZaeo-XDNT7JlIfSMZDv1xipjtbl-P0z0q4u2wYOLquvvDjRdI_9vStZK3EOmYARhDXhH0vcJNjVXYTuq16BtXsyfEW3WLBPH67t9Ef3c6cWqU3qPSS3-ddZY5VVq6pPpmtnHuBNzB5hZvZ8asMexc7S0V075ZG-7axUcwXkWKTwCZuxwZNm3VinQze4meWY6vWITtD6zHCguMIWZgxW5z7LGZ04j8_26NbBmZXV52-TFRJExi6H6kUmDb3GrYTlLTOziEUB9pl4NkCX_ghi-Pixbzg7zc_LD_cKXoUyj7iHRNfdfXLck9SOxXkmWAI7hNAswBgx1ngnH6AVyNSsFYVNF1jzc-uTwANfMQqjqq5_XCdE2Z2GFOvFJQ6JK4S-gAEpLygzeltbvYxuK_qqONA9cCTqoJlOxSgQY_lj7h6s__1EuyP9_-Fh4J9MWi9i118ndkmzaswOcxU_VfnHLDgbQbXD5B7zaS1c90=</me:sig>
// </me:env>
// </diaspora>
type
PrivateEnvMarshal
struct
{
XMLName
xml
.
Name
`xml:"me:env"`
Me
string
`xml:"xmlns:me,attr"`
Data
struct
{
XMLName
xml
.
Name
`xml:"me:data"`
Type
string
`xml:"type,attr"`
Data
string
`xml:",chardata"`
}
Encoding
string
`xml:"me:encoding"`
Alg
string
`xml:"me:alg"`
Sig
struct
{
XMLName
xml
.
Name
`xml:"me:sig"`
Sig
string
`xml:",chardata"`
KeyId
string
`xml:"key_id,attr,omitempty"`
}
}
type
PrivateMarshal
struct
{
XMLName
xml
.
Name
`xml:"diaspora"`
Xmlns
string
`xml:"xmlns,attr"`
XmlnsMe
string
`xml:"me,attr"`
EncryptedHeader
string
`xml:"encrypted_header,omitempty"`
Env
PrivateEnvMarshal
}
type
PublicMarshal
struct
{
XMLName
xml
.
Name
`xml:"diaspora"`
Xmlns
string
`xml:"xmlns,attr"`
XmlnsMe
string
`xml:"me,attr"`
Header
Header
Env
PrivateEnvMarshal
}
// NOTE I had huge problems with marshal
// and unmarshal with the same structs
// apperently namespaces in tags are a problem
...
...
header.go
View file @
d41b4df7
...
...
@@ -25,67 +25,37 @@ import(
"crypto/rand"
)
func
(
request
*
PrivateMarshal
)
EncryptHeader
(
authorId
string
,
serializedPubKey
[]
byte
)
(
err
error
)
{
// Generate the AES key before you start
// encrypting the plain header
var
aesKeySet
Aes
err
=
aesKeySet
.
Generate
()
if
err
!=
nil
{
return
}
// The actual header
//
// <decrypted_header>
// <iv>...</iv>
// <aes_key>...</aes_key>
// <author_id>one@two.tld</author_id>
// </decrypted_header>
decryptedHeader
:=
XmlDecryptedHeader
{
Iv
:
aesKeySet
.
Iv
,
AesKey
:
aesKeySet
.
Key
,
AuthorId
:
authorId
,
}
decryptedHeaderXml
,
err
:=
json
.
Marshal
(
decryptedHeader
)
if
err
!=
nil
{
return
err
}
err
=
aesKeySet
.
Encrypt
(
decryptedHeaderXml
)
if
err
!=
nil
{
return
}
aesKeySetXml
,
err
:=
json
.
Marshal
(
aesKeySet
)
if
err
!=
nil
{
return
err
}
pubKey
,
err
:=
ParseRSAPubKey
(
serializedPubKey
)
if
err
!=
nil
{
return
err
}
// aes_key
aesKey
,
err
:=
rsa
.
EncryptPKCS1v15
(
rand
.
Reader
,
pubKey
,
aesKeySetXml
)
if
err
!=
nil
{
return
err
}
aesKeyEncoded
:=
base64
.
StdEncoding
.
EncodeToString
(
aesKey
)
header
:=
JsonEnvHeader
{
AesKey
:
aesKeyEncoded
,
Ciphertext
:
aesKeySet
.
Data
,
}
headerXml
,
err
:=
json
.
Marshal
(
header
)
if
err
!=
nil
{
return
err
}
/* Header
<?xml version="1.0" encoding="UTF-8"?>
<diaspora xmlns="https://joindiaspora.com/protocol" xmlns:me="http://salmon-protocol.org/ns/magic-env">
<header>
<author_id>dia@192.168.0.173:3000</author_id>
</header>
*/
type
Header
struct
{
XMLName
xml
.
Name
`xml:"header"`
AuthorId
string
`xml:"author_id"`
}
/* Decrypted Header
<?xml version="1.0" encoding="UTF-8"?>
<decrypted_header>
<iv>...</iv>
<aes_key>...</aes_key>
<author_id>one@two.tld</author_id>
</decrypted_header>
*/
type
XmlDecryptedHeader
struct
{
XMLName
xml
.
Name
`xml:"decrypted_header"`
Iv
string
`xml:"iv"`
AesKey
string
`xml:"aes_key"`
AuthorId
string
`xml:"author_id"`
}
(
*
request
)
.
EncryptedHeader
=
base64
.
StdEncoding
.
EncodeToString
(
headerXml
)
return
type
JsonEnvHeader
struct
{
AesKey
string
`json:"aes_key"`
Ciphertext
string
`json:"ciphertext"`
}
func
(
request
*
DiasporaUnmarshal
)
DecryptHeader
(
serialized
[]
byte
)
error
{
...
...
http_client.go
View file @
d41b4df7
...
...
@@ -30,36 +30,38 @@ import (
const
(
PROTO_HTTP
=
"http://"
PROTO_HTTPS
=
"https://"
CONTENT_TYPE_ENVELOPE
=
"application/magic-envelope+xml"
CONTENT_TYPE_JSON
=
"application/json"
CONTENT_TYPE_FORM
=
"application/x-www-form-urlencoded"
)
var
timeout
=
time
.
Duration
(
10
*
time
.
Second
)
func
Push
Xml
ToPrivate
(
host
,
guid
string
,
body
io
.
Reader
)
error
{
return
push
Xml
(
host
,
"/receive/users/"
+
guid
,
PROTO_HTTPS
,
body
)
func
Push
Json
ToPrivate
(
host
,
guid
string
,
body
io
.
Reader
)
error
{
return
push
(
host
,
"/receive/users/"
+
guid
,
PROTO_HTTPS
,
CONTENT_TYPE_JSON
,
body
)
}
func
PushXmlToPublic
(
host
string
,
body
io
.
Reader
)
error
{
return
push
Xml
(
host
,
"/receive/public"
,
PROTO_HTTPS
,
body
)
return
push
(
host
,
"/receive/public"
,
PROTO_HTTPS
,
CONTENT_TYPE_ENVELOPE
,
body
)
}
func
pushXml
(
host
,
endpoint
,
proto
string
,
body
io
.
Reader
)
error
{
func
PushFormToPrivate
(
host
,
guid
string
,
body
io
.
Reader
)
error
{
return
push
(
host
,
"/receive/users/"
+
guid
,
PROTO_HTTPS
,
CONTENT_TYPE_FORM
,
body
)
}
func
push
(
host
,
endpoint
,
proto
,
contentType
string
,
body
io
.
Reader
)
error
{
req
,
err
:=
http
.
NewRequest
(
"POST"
,
proto
+
host
+
endpoint
,
body
)
if
err
!=
nil
{
return
err
}
if
strings
.
Contains
(
endpoint
,
"public"
)
{
req
.
Header
.
Add
(
"Content-Type"
,
"application/magic-envelope+xml"
)
}
else
{
req
.
Header
.
Add
(
"Content-Type"
,
"application/json"
)
}
req
.
Header
.
Add
(
"Content-Type"
,
contentType
)
client
:=
&
http
.
Client
{
Timeout
:
timeout
}
resp
,
err
:=
client
.
Do
(
req
)
if
err
!=
nil
{
if
proto
==
PROTO_HTTPS
{
info
(
"Retry with"
,
PROTO_HTTP
,
"on"
,
host
,
err
)
return
push
Xml
(
host
,
endpoint
,
PROTO_HTTP
,
body
)
return
push
(
host
,
endpoint
,
PROTO_HTTP
,
contentType
,
body
)
}
return
err
}
...
...
magic.go
View file @
d41b4df7
...
...
@@ -20,13 +20,28 @@ package federation
import
(
"encoding/base64"
"encoding/xml"
//XXX
"encoding/json"
"crypto/rsa"
"crypto/rand"
)
type
MagicEnvelopeMarshal
struct
{
XMLName
xml
.
Name
`xml:"me:env"`
Me
string
`xml:"xmlns:me,attr"`
Data
struct
{
XMLName
xml
.
Name
`xml:"me:data"`
Type
string
`xml:"type,attr"`
Data
string
`xml:",chardata"`
}
Encoding
string
`xml:"me:encoding"`
Alg
string
`xml:"me:alg"`
Sig
struct
{
XMLName
xml
.
Name
`xml:"me:sig"`
Sig
string
`xml:",chardata"`
KeyId
string
`xml:"key_id,attr,omitempty"`
}
}
func
MagicEnvelope
(
privkey
,
handle
string
,
plainXml
[]
byte
)
(
payload
[]
byte
,
err
error
)
{
info
(
"plain xml"
,
string
(
plainXml
))
info
(
"privkey length"
,
len
(
privkey
))
...
...
@@ -35,7 +50,7 @@ func MagicEnvelope(privkey, handle string, plainXml []byte) (payload []byte, err
data
:=
base64
.
URLEncoding
.
EncodeToString
(
plainXml
)
keyId
:=
base64
.
URLEncoding
.
EncodeToString
([]
byte
(
handle
))
xmlBody
:=
PrivateEnv
Marshal
{}
xmlBody
:=
MagicEnvelope
Marshal
{}
xmlBody
.
Data
.
Type
=
APPLICATION_XML
xmlBody
.
Data
.
Data
=
data
xmlBody
.
Me
=
XMLNS_ME
...
...
@@ -59,6 +74,9 @@ func MagicEnvelope(privkey, handle string, plainXml []byte) (payload []byte, err
}
func
EncryptedMagicEnvelope
(
privkey
,
pubkey
,
handle
string
,
serializedXml
[]
byte
)
(
payload
[]
byte
,
err
error
)
{
var
aesKeySet
Aes
var
aesWrapper
AesWrapper
info
(
"serialized xml"
,
string
(
serializedXml
))
info
(
"privkey length"
,
len
(
privkey
))
info
(
"pubkey length"
,
len
(
pubkey
))
...
...
@@ -67,58 +85,46 @@ func EncryptedMagicEnvelope(privkey, pubkey, handle string, serializedXml []byte
data
:=
base64
.
URLEncoding
.
EncodeToString
(
serializedXml
)
keyId
:=
base64
.
URLEncoding
.
EncodeToString
([]
byte
(
handle
))
// encrypted header
//xmlBody := PrivateMarshal{
// Xmlns: XMLNS,
// XmlnsMe: XMLNS_ME,
xmlBodyEnv
:=
PrivateEnvMarshal
{
envelope
:=
MagicEnvelopeMarshal
{
Me
:
XMLNS_ME
,
Encoding
:
BASE64_URL
,
Alg
:
RSA_SHA256
,
}
//}
xmlBodyEnv
.
Data
.
Type
=
APPLICATION_XML
xmlBodyEnv
.
Data
.
Data
=
data
xmlBodyEnv
.
Sig
.
KeyId
=
keyId
envelope
.
Data
.
Type
=
APPLICATION_XML
envelope
.
Data
.
Data
=
data
envelope
.
Sig
.
KeyId
=
keyId
err
=
xmlBodyEnv
.
Sign
(
privkey
)
err
=
envelope
.
Sign
(
privkey
)
if
err
!=
nil
{
warn
(
err
)
return
}
// XXX NOTE move below to header file
// Generate a new AES key pair
var
(
aesKeySet
Aes
aesWrapper
AesWrapper
)
err
=
aesKeySet
.
Generate
()
if
err
!=
nil
{
warn
(
err
)
return
}
// payload
mit aes versch und base64
payload
,
err
=
xml
.
Marshal
(
xmlBodyEnv
)
// payload
with aes encryption
payload
,
err
=
xml
.
Marshal
(
envelope
)
if
err
!=
nil
{
warn
(
err
)
return
}
info
(
"payload, err = xml.Marshal(envelope) "
,
string
(
payload
))
err
=
aesKeySet
.
Encrypt
(
payload
)
if
err
!=
nil
{
warn
(
err
)
return
}
//aesWrapper.MagicEnvelope = base64.StdEncoding.EncodeToString([]byte(aesKeySet.Data))
aesWrapper
.
MagicEnvelope
=
aesKeySet
.
Data
// aes mit pub key versch
// aes with rsa encryption
aesKeySetXml
,
err
:=
json
.
Marshal
(
aesKeySet
)
if
err
!=
nil
{
warn
(
err
)
...
...
@@ -133,23 +139,19 @@ func EncryptedMagicEnvelope(privkey, pubkey, handle string, serializedXml []byte
info
(
"aesKeySetXml"
,
string
(
aesKeySetXml
))
// aes_key
aesKey
,
err
:=
rsa
.
EncryptPKCS1v15
(
rand
.
Reader
,
pubKey
,
aesKeySetXml
)
if
err
!=
nil
{
warn
(
err
)
return
}
aesWrapper
.
AesKey
=
base64
.
StdEncoding
.
EncodeToString
(
aesKey
)
payload
,
err
=
json
.
Marshal
(
aesWrapper
)
if
err
!=
nil
{
warn
(
err
)
return
}
info
(
"payload"
,
string
(
payload
))
return
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment