Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
W
wine-winehq
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Registry
Registry
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
wine
wine-winehq
Commits
66baf153
Commit
66baf153
authored
Sep 04, 2020
by
Hans Leidekker
Committed by
Alexandre Julliard
Sep 04, 2020
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
advapi32: Enumerate host credentials through mountmgr.
Signed-off-by:
Hans Leidekker
<
hans@codeweavers.com
>
Signed-off-by:
Alexandre Julliard
<
julliard@winehq.org
>
parent
3885b32b
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
115 additions
and
297 deletions
+115
-297
Makefile.in
dlls/advapi32/Makefile.in
+0
-1
cred.c
dlls/advapi32/cred.c
+115
-296
No files found.
dlls/advapi32/Makefile.in
View file @
66baf153
...
...
@@ -3,7 +3,6 @@ MODULE = advapi32.dll
IMPORTLIB
=
advapi32
IMPORTS
=
kernelbase sechost
DELAYIMPORTS
=
rpcrt4
EXTRALIBS
=
$(SECURITY_LIBS)
C_SRCS
=
\
advapi.c
\
...
...
dlls/advapi32/cred.c
View file @
66baf153
...
...
@@ -22,12 +22,6 @@
#include <time.h>
#include <limits.h>
#ifdef __APPLE__
# include <Security/SecKeychain.h>
# include <Security/SecKeychainItem.h>
# include <Security/SecKeychainSearch.h>
#endif
#include "windef.h"
#include "winbase.h"
#include "winreg.h"
...
...
@@ -237,189 +231,6 @@ static DWORD registry_read_credential(HKEY hkey, PCREDENTIALW credential,
return
ret
;
}
#ifdef __APPLE__
static
DWORD
mac_read_credential_from_item
(
SecKeychainItemRef
item
,
BOOL
require_password
,
PCREDENTIALW
credential
,
char
*
buffer
,
DWORD
*
len
)
{
int
status
;
UInt32
i
,
cred_blob_len
;
void
*
cred_blob
;
WCHAR
*
user
=
NULL
;
BOOL
user_name_present
=
FALSE
;
SecKeychainAttributeInfo
info
;
SecKeychainAttributeList
*
attr_list
;
UInt32
info_tags
[]
=
{
kSecServiceItemAttr
,
kSecAccountItemAttr
,
kSecCommentItemAttr
,
kSecCreationDateItemAttr
};
info
.
count
=
ARRAY_SIZE
(
info_tags
);
info
.
tag
=
info_tags
;
info
.
format
=
NULL
;
status
=
SecKeychainItemCopyAttributesAndData
(
item
,
&
info
,
NULL
,
&
attr_list
,
&
cred_blob_len
,
&
cred_blob
);
if
(
status
==
errSecAuthFailed
&&
!
require_password
)
{
cred_blob_len
=
0
;
cred_blob
=
NULL
;
status
=
SecKeychainItemCopyAttributesAndData
(
item
,
&
info
,
NULL
,
&
attr_list
,
&
cred_blob_len
,
NULL
);
}
if
(
status
!=
noErr
)
{
WARN
(
"SecKeychainItemCopyAttributesAndData returned status %d
\n
"
,
status
);
return
ERROR_NOT_FOUND
;
}
for
(
i
=
0
;
i
<
attr_list
->
count
;
i
++
)
if
(
attr_list
->
attr
[
i
].
tag
==
kSecAccountItemAttr
&&
attr_list
->
attr
[
i
].
data
)
{
user_name_present
=
TRUE
;
break
;
}
if
(
!
user_name_present
)
{
WARN
(
"no kSecAccountItemAttr for item
\n
"
);
SecKeychainItemFreeAttributesAndData
(
attr_list
,
cred_blob
);
return
ERROR_NOT_FOUND
;
}
if
(
buffer
)
{
credential
->
Flags
=
0
;
credential
->
Type
=
CRED_TYPE_DOMAIN_PASSWORD
;
credential
->
TargetName
=
NULL
;
credential
->
Comment
=
NULL
;
memset
(
&
credential
->
LastWritten
,
0
,
sizeof
(
credential
->
LastWritten
));
credential
->
CredentialBlobSize
=
0
;
credential
->
CredentialBlob
=
NULL
;
credential
->
Persist
=
CRED_PERSIST_LOCAL_MACHINE
;
credential
->
AttributeCount
=
0
;
credential
->
Attributes
=
NULL
;
credential
->
TargetAlias
=
NULL
;
credential
->
UserName
=
NULL
;
}
for
(
i
=
0
;
i
<
attr_list
->
count
;
i
++
)
{
switch
(
attr_list
->
attr
[
i
].
tag
)
{
case
kSecServiceItemAttr
:
TRACE
(
"kSecServiceItemAttr: %.*s
\n
"
,
(
int
)
attr_list
->
attr
[
i
].
length
,
(
char
*
)
attr_list
->
attr
[
i
].
data
);
if
(
!
attr_list
->
attr
[
i
].
data
)
continue
;
if
(
buffer
)
{
INT
str_len
;
credential
->
TargetName
=
(
LPWSTR
)
buffer
;
str_len
=
MultiByteToWideChar
(
CP_UTF8
,
0
,
attr_list
->
attr
[
i
].
data
,
attr_list
->
attr
[
i
].
length
,
(
LPWSTR
)
buffer
,
0xffff
);
credential
->
TargetName
[
str_len
]
=
'\0'
;
buffer
+=
(
str_len
+
1
)
*
sizeof
(
WCHAR
);
*
len
+=
(
str_len
+
1
)
*
sizeof
(
WCHAR
);
}
else
{
INT
str_len
;
str_len
=
MultiByteToWideChar
(
CP_UTF8
,
0
,
attr_list
->
attr
[
i
].
data
,
attr_list
->
attr
[
i
].
length
,
NULL
,
0
);
*
len
+=
(
str_len
+
1
)
*
sizeof
(
WCHAR
);
}
break
;
case
kSecAccountItemAttr
:
{
INT
str_len
;
TRACE
(
"kSecAccountItemAttr: %.*s
\n
"
,
(
int
)
attr_list
->
attr
[
i
].
length
,
(
char
*
)
attr_list
->
attr
[
i
].
data
);
if
(
!
attr_list
->
attr
[
i
].
data
)
continue
;
str_len
=
MultiByteToWideChar
(
CP_UTF8
,
0
,
attr_list
->
attr
[
i
].
data
,
attr_list
->
attr
[
i
].
length
,
NULL
,
0
);
user
=
heap_alloc
((
str_len
+
1
)
*
sizeof
(
WCHAR
));
MultiByteToWideChar
(
CP_UTF8
,
0
,
attr_list
->
attr
[
i
].
data
,
attr_list
->
attr
[
i
].
length
,
user
,
str_len
);
user
[
str_len
]
=
'\0'
;
break
;
}
case
kSecCommentItemAttr
:
TRACE
(
"kSecCommentItemAttr: %.*s
\n
"
,
(
int
)
attr_list
->
attr
[
i
].
length
,
(
char
*
)
attr_list
->
attr
[
i
].
data
);
if
(
!
attr_list
->
attr
[
i
].
data
)
continue
;
if
(
buffer
)
{
INT
str_len
;
credential
->
Comment
=
(
LPWSTR
)
buffer
;
str_len
=
MultiByteToWideChar
(
CP_UTF8
,
0
,
attr_list
->
attr
[
i
].
data
,
attr_list
->
attr
[
i
].
length
,
(
LPWSTR
)
buffer
,
0xffff
);
credential
->
Comment
[
str_len
]
=
'\0'
;
buffer
+=
(
str_len
+
1
)
*
sizeof
(
WCHAR
);
*
len
+=
(
str_len
+
1
)
*
sizeof
(
WCHAR
);
}
else
{
INT
str_len
;
str_len
=
MultiByteToWideChar
(
CP_UTF8
,
0
,
attr_list
->
attr
[
i
].
data
,
attr_list
->
attr
[
i
].
length
,
NULL
,
0
);
*
len
+=
(
str_len
+
1
)
*
sizeof
(
WCHAR
);
}
break
;
case
kSecCreationDateItemAttr
:
TRACE
(
"kSecCreationDateItemAttr: %.*s
\n
"
,
(
int
)
attr_list
->
attr
[
i
].
length
,
(
char
*
)
attr_list
->
attr
[
i
].
data
);
if
(
!
attr_list
->
attr
[
i
].
data
)
continue
;
if
(
buffer
)
{
LARGE_INTEGER
win_time
;
struct
tm
tm
;
time_t
time
;
memset
(
&
tm
,
0
,
sizeof
(
tm
));
strptime
(
attr_list
->
attr
[
i
].
data
,
"%Y%m%d%H%M%SZ"
,
&
tm
);
time
=
mktime
(
&
tm
);
RtlSecondsSince1970ToTime
(
time
,
&
win_time
);
credential
->
LastWritten
.
dwLowDateTime
=
win_time
.
u
.
LowPart
;
credential
->
LastWritten
.
dwHighDateTime
=
win_time
.
u
.
HighPart
;
}
break
;
default:
FIXME
(
"unhandled attribute %u
\n
"
,
(
unsigned
)
attr_list
->
attr
[
i
].
tag
);
break
;
}
}
if
(
user
)
{
INT
str_len
;
if
(
buffer
)
credential
->
UserName
=
(
LPWSTR
)
buffer
;
str_len
=
strlenW
(
user
);
*
len
+=
(
str_len
+
1
)
*
sizeof
(
WCHAR
);
if
(
buffer
)
{
memcpy
(
buffer
,
user
,
(
str_len
+
1
)
*
sizeof
(
WCHAR
));
buffer
+=
(
str_len
+
1
)
*
sizeof
(
WCHAR
);
TRACE
(
"UserName = %s
\n
"
,
debugstr_w
(
credential
->
UserName
));
}
}
heap_free
(
user
);
if
(
cred_blob
)
{
if
(
buffer
)
{
INT
str_len
;
credential
->
CredentialBlob
=
(
BYTE
*
)
buffer
;
str_len
=
MultiByteToWideChar
(
CP_UTF8
,
0
,
cred_blob
,
cred_blob_len
,
(
LPWSTR
)
buffer
,
0xffff
);
credential
->
CredentialBlobSize
=
str_len
*
sizeof
(
WCHAR
);
*
len
+=
str_len
*
sizeof
(
WCHAR
);
}
else
{
INT
str_len
;
str_len
=
MultiByteToWideChar
(
CP_UTF8
,
0
,
cred_blob
,
cred_blob_len
,
NULL
,
0
);
*
len
+=
str_len
*
sizeof
(
WCHAR
);
}
}
SecKeychainItemFreeAttributesAndData
(
attr_list
,
cred_blob
);
return
ERROR_SUCCESS
;
}
#endif
static
DWORD
write_credential_blob
(
HKEY
hkey
,
LPCWSTR
target_name
,
DWORD
type
,
const
BYTE
key_data
[
KEY_SIZE
],
const
BYTE
*
credential_blob
,
DWORD
credential_blob_size
)
...
...
@@ -736,98 +547,6 @@ static DWORD registry_enumerate_credentials(HKEY hkeyMgr, LPCWSTR filter,
return
ret
;
}
#ifdef __APPLE__
static
BOOL
mac_credential_matches_filter
(
void
*
data
,
UInt32
data_len
,
const
WCHAR
*
filter
)
{
int
len
;
WCHAR
*
target_name
;
const
WCHAR
*
p
;
BOOL
ret
;
if
(
!
filter
)
return
TRUE
;
len
=
MultiByteToWideChar
(
CP_UTF8
,
0
,
data
,
data_len
,
NULL
,
0
);
if
(
!
(
target_name
=
heap_alloc
((
len
+
1
)
*
sizeof
(
WCHAR
))))
return
FALSE
;
MultiByteToWideChar
(
CP_UTF8
,
0
,
data
,
data_len
,
target_name
,
len
);
target_name
[
len
]
=
0
;
TRACE
(
"comparing filter %s to target name %s
\n
"
,
debugstr_w
(
filter
),
debugstr_w
(
target_name
));
p
=
strchrW
(
filter
,
'*'
);
ret
=
CompareStringW
(
GetThreadLocale
(),
NORM_IGNORECASE
,
filter
,
(
p
&&
!
p
[
1
]
?
p
-
filter
:
-
1
),
target_name
,
(
p
&&
!
p
[
1
]
?
p
-
filter
:
-
1
))
==
CSTR_EQUAL
;
heap_free
(
target_name
);
return
ret
;
}
static
DWORD
mac_enumerate_credentials
(
LPCWSTR
filter
,
PCREDENTIALW
*
credentials
,
char
*
buffer
,
DWORD
*
len
,
DWORD
*
count
)
{
SecKeychainSearchRef
search
;
SecKeychainItemRef
item
;
int
status
;
Boolean
saved_user_interaction_allowed
;
DWORD
ret
;
SecKeychainGetUserInteractionAllowed
(
&
saved_user_interaction_allowed
);
SecKeychainSetUserInteractionAllowed
(
false
);
status
=
SecKeychainSearchCreateFromAttributes
(
NULL
,
kSecGenericPasswordItemClass
,
NULL
,
&
search
);
if
(
status
==
noErr
)
{
while
(
SecKeychainSearchCopyNext
(
search
,
&
item
)
==
noErr
)
{
SecKeychainAttributeInfo
info
;
SecKeychainAttributeList
*
attr_list
;
UInt32
info_tags
[]
=
{
kSecServiceItemAttr
};
BOOL
match
;
info
.
count
=
ARRAY_SIZE
(
info_tags
);
info
.
tag
=
info_tags
;
info
.
format
=
NULL
;
status
=
SecKeychainItemCopyAttributesAndData
(
item
,
&
info
,
NULL
,
&
attr_list
,
NULL
,
NULL
);
if
(
status
!=
noErr
)
{
WARN
(
"SecKeychainItemCopyAttributesAndData returned status %d
\n
"
,
status
);
continue
;
}
if
(
buffer
)
{
*
len
=
sizeof
(
CREDENTIALW
);
credentials
[
*
count
]
=
(
PCREDENTIALW
)
buffer
;
}
else
*
len
+=
sizeof
(
CREDENTIALW
);
if
(
attr_list
->
count
!=
1
||
attr_list
->
attr
[
0
].
tag
!=
kSecServiceItemAttr
)
{
SecKeychainItemFreeAttributesAndData
(
attr_list
,
NULL
);
continue
;
}
TRACE
(
"service item: %.*s
\n
"
,
(
int
)
attr_list
->
attr
[
0
].
length
,
(
char
*
)
attr_list
->
attr
[
0
].
data
);
match
=
mac_credential_matches_filter
(
attr_list
->
attr
[
0
].
data
,
attr_list
->
attr
[
0
].
length
,
filter
);
SecKeychainItemFreeAttributesAndData
(
attr_list
,
NULL
);
if
(
!
match
)
continue
;
ret
=
mac_read_credential_from_item
(
item
,
FALSE
,
buffer
?
credentials
[
*
count
]
:
NULL
,
buffer
?
buffer
+
sizeof
(
CREDENTIALW
)
:
NULL
,
len
);
CFRelease
(
item
);
if
(
ret
==
ERROR_SUCCESS
)
{
(
*
count
)
++
;
if
(
buffer
)
buffer
+=
*
len
;
}
}
CFRelease
(
search
);
}
else
ERR
(
"SecKeychainSearchCreateFromAttributes returned status %d
\n
"
,
status
);
SecKeychainSetUserInteractionAllowed
(
saved_user_interaction_allowed
);
return
ERROR_SUCCESS
;
}
#endif
/******************************************************************************
* convert_PCREDENTIALW_to_PCREDENTIALA [internal]
*
...
...
@@ -1194,11 +913,113 @@ BOOL WINAPI CredEnumerateA(LPCSTR Filter, DWORD Flags, DWORD *Count,
return
TRUE
;
}
#define CRED_LIST_COUNT 16
#define CRED_DATA_SIZE 2048
static
DWORD
host_enumerate_credentials
(
const
WCHAR
*
filter
,
CREDENTIALW
**
credentials
,
char
*
buf
,
DWORD
*
len
,
DWORD
*
count
)
{
static
const
WCHAR
emptyW
[]
=
{
0
};
struct
mountmgr_credential_list
*
list
,
*
tmp
;
DWORD
i
,
j
,
ret
,
size
,
filter_size
,
offset
=
0
;
HANDLE
mgr
;
WCHAR
*
ptr
;
if
(
filter
)
filter_size
=
(
strlenW
(
filter
)
+
1
)
*
sizeof
(
WCHAR
);
else
{
filter
=
emptyW
;
filter_size
=
sizeof
(
emptyW
);
}
mgr
=
CreateFileW
(
MOUNTMGR_DOS_DEVICE_NAME
,
GENERIC_READ
|
GENERIC_WRITE
,
FILE_SHARE_READ
|
FILE_SHARE_WRITE
,
NULL
,
OPEN_EXISTING
,
0
,
0
);
if
(
mgr
==
INVALID_HANDLE_VALUE
)
return
GetLastError
();
size
=
FIELD_OFFSET
(
struct
mountmgr_credential_list
,
creds
[
CRED_LIST_COUNT
]
)
+
filter_size
+
CRED_DATA_SIZE
;
if
(
!
(
list
=
heap_alloc
(
size
)))
{
CloseHandle
(
mgr
);
return
ERROR_OUTOFMEMORY
;
}
for
(;;)
{
list
->
filter_offset
=
sizeof
(
*
list
);
list
->
filter_size
=
filter_size
;
ptr
=
(
WCHAR
*
)((
char
*
)
list
+
list
->
filter_offset
);
strcpyW
(
ptr
,
filter
);
if
(
DeviceIoControl
(
mgr
,
IOCTL_MOUNTMGR_ENUMERATE_CREDENTIALS
,
list
,
size
,
list
,
size
,
NULL
,
NULL
))
break
;
if
((
ret
=
GetLastError
())
!=
ERROR_MORE_DATA
)
goto
done
;
size
=
list
->
size
+
filter_size
;
if
(
!
(
tmp
=
heap_realloc
(
list
,
size
)))
{
ret
=
ERROR_OUTOFMEMORY
;
goto
done
;
}
list
=
tmp
;
}
for
(
i
=
0
,
j
=
*
count
;
i
<
list
->
count
;
i
++
)
{
CREDENTIALW
*
cred
=
(
CREDENTIALW
*
)(
buf
+
offset
);
offset
+=
sizeof
(
*
cred
)
+
list
->
creds
[
i
].
targetname_size
+
list
->
creds
[
i
].
comment_size
+
list
->
creds
[
i
].
blob_size
+
list
->
creds
[
i
].
username_size
;
if
(
!
buf
)
continue
;
ptr
=
(
WCHAR
*
)(
cred
+
1
);
cred
->
Flags
=
0
;
cred
->
Type
=
CRED_TYPE_DOMAIN_PASSWORD
;
cred
->
TargetName
=
ptr
;
memcpy
(
cred
->
TargetName
,
(
char
*
)
&
list
->
creds
[
i
]
+
list
->
creds
[
i
].
targetname_offset
,
list
->
creds
[
i
].
targetname_size
);
ptr
+=
list
->
creds
[
i
].
targetname_size
/
sizeof
(
WCHAR
);
if
(
list
->
creds
[
i
].
comment_size
)
{
cred
->
Comment
=
ptr
;
memcpy
(
cred
->
Comment
,
(
char
*
)
&
list
->
creds
[
i
]
+
list
->
creds
[
i
].
comment_offset
,
list
->
creds
[
i
].
comment_size
);
ptr
+=
list
->
creds
[
i
].
comment_size
/
sizeof
(
WCHAR
);
}
else
cred
->
Comment
=
NULL
;
cred
->
LastWritten
=
list
->
creds
[
i
].
last_written
;
if
(
list
->
creds
[
i
].
blob_size
)
{
cred
->
CredentialBlobSize
=
list
->
creds
[
i
].
blob_size
;
cred
->
CredentialBlob
=
(
BYTE
*
)
ptr
;
memcpy
(
cred
->
CredentialBlob
,
(
char
*
)
&
list
->
creds
[
i
]
+
list
->
creds
[
i
].
blob_offset
,
list
->
creds
[
i
].
blob_size
);
ptr
+=
list
->
creds
[
i
].
blob_size
/
sizeof
(
WCHAR
);
}
else
{
cred
->
CredentialBlobSize
=
0
;
cred
->
CredentialBlob
=
NULL
;
}
cred
->
Persist
=
CRED_PERSIST_LOCAL_MACHINE
;
cred
->
AttributeCount
=
0
;
cred
->
Attributes
=
NULL
;
cred
->
TargetAlias
=
NULL
;
if
(
list
->
creds
[
i
].
username_size
)
{
cred
->
UserName
=
ptr
;
memcpy
(
cred
->
UserName
,
(
char
*
)
&
list
->
creds
[
i
]
+
list
->
creds
[
i
].
username_offset
,
list
->
creds
[
i
].
username_size
);
}
else
cred
->
UserName
=
NULL
;
if
(
credentials
)
credentials
[
j
++
]
=
cred
;
}
*
len
+=
offset
;
*
count
+=
list
->
count
;
ret
=
ERROR_SUCCESS
;
done:
heap_free
(
list
);
CloseHandle
(
mgr
);
return
ret
;
}
/******************************************************************************
* CredEnumerateW [ADVAPI32.@]
*/
BOOL
WINAPI
CredEnumerateW
(
LPCWSTR
Filter
,
DWORD
Flags
,
DWORD
*
Count
,
PCREDENTIALW
**
Credentials
)
BOOL
WINAPI
CredEnumerateW
(
LPCWSTR
Filter
,
DWORD
Flags
,
DWORD
*
Count
,
PCREDENTIALW
**
Credentials
)
{
HKEY
hkeyMgr
;
DWORD
ret
;
...
...
@@ -1252,10 +1073,11 @@ BOOL WINAPI CredEnumerateW(LPCWSTR Filter, DWORD Flags, DWORD *Count,
len
=
0
;
ret
=
registry_enumerate_credentials
(
hkeyMgr
,
Filter
,
target_name
,
target_name_len
,
key_data
,
NULL
,
NULL
,
&
len
,
Count
);
#ifdef __APPLE__
if
(
ret
==
ERROR_SUCCESS
)
ret
=
mac_enumerate_credentials
(
Filter
,
NULL
,
NULL
,
&
len
,
Count
);
#endif
{
ret
=
host_enumerate_credentials
(
Filter
,
NULL
,
NULL
,
&
len
,
Count
);
if
(
ret
==
ERROR_NOT_SUPPORTED
)
ret
=
ERROR_SUCCESS
;
}
if
(
ret
==
ERROR_SUCCESS
&&
*
Count
==
0
)
ret
=
ERROR_NOT_FOUND
;
if
(
ret
!=
ERROR_SUCCESS
)
...
...
@@ -1275,18 +1097,15 @@ BOOL WINAPI CredEnumerateW(LPCWSTR Filter, DWORD Flags, DWORD *Count,
{
buffer
+=
*
Count
*
sizeof
(
PCREDENTIALW
);
*
Count
=
0
;
ret
=
registry_enumerate_credentials
(
hkeyMgr
,
Filter
,
target_name
,
target_name_len
,
key_data
,
*
Credentials
,
&
buffer
,
&
len
,
Count
);
#ifdef __APPLE__
ret
=
registry_enumerate_credentials
(
hkeyMgr
,
Filter
,
target_name
,
target_name_len
,
key_data
,
*
Credentials
,
&
buffer
,
&
len
,
Count
);
if
(
ret
==
ERROR_SUCCESS
)
ret
=
mac_enumerate_credentials
(
Filter
,
*
Credentials
,
buffer
,
&
len
,
Count
);
#endif
{
ret
=
host_enumerate_credentials
(
Filter
,
*
Credentials
,
buffer
,
&
len
,
Count
);
if
(
ret
==
ERROR_NOT_SUPPORTED
)
ret
=
ERROR_SUCCESS
;
}
}
else
ret
=
ERROR_OUTOFMEMORY
;
else
ret
=
ERROR_OUTOFMEMORY
;
}
heap_free
(
target_name
);
...
...
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