如何在JNA中映射Windows API CredWrite / CredRead?

时间:2016-07-15 20:23:13

标签: java winapi jna credentials

我试图在JNA中映射CredWrite / CredRead,以便在Windows Credential Manager(OS Windows 10)中存储我的Java应用程序中使用的第三方凭证。

这里是C:

中的原始签名
// https://msdn.microsoft.com/en-us/library/aa375187(v=vs.85).aspx
BOOL CredWrite(
  _In_ PCREDENTIAL Credential,
  _In_ DWORD       Flags
);

// https://msdn.microsoft.com/en-us/library/aa374804(v=vs.85).aspx
BOOL CredRead(
  _In_  LPCTSTR     TargetName,
  _In_  DWORD       Type,
  _In_  DWORD       Flags,
  _Out_ PCREDENTIAL *Credential
);

typedef struct _CREDENTIAL {
  DWORD                 Flags;
  DWORD                 Type;
  LPTSTR                TargetName;
  LPTSTR                Comment;
  FILETIME              LastWritten;
  DWORD                 CredentialBlobSize;
  LPBYTE                CredentialBlob;
  DWORD                 Persist;
  DWORD                 AttributeCount;
  PCREDENTIAL_ATTRIBUTE Attributes;
  LPTSTR                TargetAlias;
  LPTSTR                UserName;
} CREDENTIAL, *PCREDENTIAL;

typedef struct _CREDENTIAL_ATTRIBUTE {
  LPTSTR Keyword;
  DWORD  Flags;
  DWORD  ValueSize;
  LPBYTE Value;
} CREDENTIAL_ATTRIBUTE, *PCREDENTIAL_ATTRIBUTE;

这里是我在Java中的地图:

WinCrypt instance = (WinCrypt) Native.loadLibrary("Advapi32", WinCrypt.class, W32APIOptions.DEFAULT_OPTIONS);

public boolean CredWrite(
        CREDENTIAL.ByReference Credential,
        int Flags
        );

public boolean CredRead(
        String TargetName,
        int Type,
        int Flags,
        PointerByReference Credential
        );

public static class CREDENTIAL extends Structure {
    public int Flags;
    public int Type;
    public String TargetName;
    public String Comment;
    public FILETIME LastWritten;
    public int CredentialBlobSize;
    public byte[] CredentialBlob = new byte[128];
    public int Persist;
    public int AttributeCount;
    public CREDENTIAL_ATTRIBUTE.ByReference Attributes;
    public String TargetAlias;
    public String UserName;

    public static class ByReference extends CREDENTIAL implements Structure.ByReference {
        public ByReference() {
        }

        public ByReference(Pointer memory) {
            super(memory);                      // LINE 55
        }
    }

    public CREDENTIAL() {
        super();
    }

    public CREDENTIAL(Pointer memory) {
        super(memory); 
        read();                                 // LINE 65
    }

    @Override
    protected List<String> getFieldOrder() {
        return Arrays.asList(new String[] {
                "Flags",
                "Type",
                "TargetName",
                "Comment",
                "LastWritten",
                "CredentialBlobSize",
                "CredentialBlob",
                "Persist",
                "AttributeCount",
                "Attributes",
                "TargetAlias",
                "UserName"
        });
    }
}

public static class CREDENTIAL_ATTRIBUTE extends Structure {
    public String Keyword;
    public int Flags;
    public int ValueSize;
    public byte[] Value = new byte[128];

    public static class ByReference extends CREDENTIAL_ATTRIBUTE implements Structure.ByReference {
    }

    @Override
    protected List<String> getFieldOrder() {
        return Arrays.asList(new String[] {
                "Keyword",
                "Flags",
                "ValueSize",
                "Value"
        });
    }
}

首先,我尝试将凭据写入Windows Credential Manager:

String password = "passwordtest";
int cbCreds = 1 + password.length();

CREDENTIAL.ByReference credRef = new CREDENTIAL.ByReference();
credRef.Type = WinCrypt.CRED_TYPE_GENERIC;
credRef.TargetName = "TEST/account";
credRef.CredentialBlobSize = cbCreds;
credRef.CredentialBlob = password.getBytes();
credRef.Persist = WinCrypt.CRED_PERSIST_LOCAL_MACHINE;
credRef.UserName = "administrator";

boolean ok = WinCrypt.instance.CredWrite(credRef, 0);
int rc = Kernel32.INSTANCE.GetLastError();
String errMsg = Kernel32Util.formatMessage(rc);
System.out.println("CredWrite() - ok: " + ok + ", errno: " + rc + ", errmsg: " + errMsg);

尝试写的输出:

CredWrite() - ok: false, errno: 87, errmsg: The parameter is incorrect.

然后我尝试从Windows Credential Manager中读取现有凭据:

PointerByReference pref = new PointerByReference();
boolean ok = WinCrypt.instance.CredRead("build-apps", WinCrypt.CRED_TYPE_DOMAIN_PASSWORD, 0, pref);
int rc = Kernel32.INSTANCE.GetLastError();
String errMsg = Kernel32Util.formatMessage(rc);
System.out.println("CredRead() - ok: " + ok + ", errno: " + rc + ", errmsg: " + errMsg);
CREDENTIAL cred = new CREDENTIAL.ByReference(pref.getPointer());        // LINE 44

尝试阅读的输出:

CredRead() - ok: true, errno: 0, errmsg: The operation completed successfully.
Exception in thread "main" java.lang.IllegalArgumentException: Structure exceeds provided memory bounds
    at com.sun.jna.Structure.ensureAllocated(Structure.java:366)
    at com.sun.jna.Structure.ensureAllocated(Structure.java:346)
    at com.sun.jna.Structure.read(Structure.java:552)
    at com.abc.crypt.WinCrypt$CREDENTIAL.<init>(WinCrypt.java:65)
    at com.abc.crypt.WinCrypt$CREDENTIAL$ByReference.<init>(WinCrypt.java:55) 
    at com.abc.crypt.CryptTest.main(CryptTest.java:44)
Caused by: java.lang.IndexOutOfBoundsException: Bounds exceeds available space : size=8, offset=200
    at com.sun.jna.Memory.boundsCheck(Memory.java:203)
    at com.sun.jna.Memory$SharedMemory.boundsCheck(Memory.java:87)
    at com.sun.jna.Memory.share(Memory.java:131)
    at com.sun.jna.Structure.ensureAllocated(Structure.java:363)
    ... 5 more

因此尝试写入失败,尝试读取成功但无法基于输出创建CREDENTIAL对象。

根据CredWrite API的网页,我在写测试中得到的错误87是以下错误:

  

ERROR_INVALID_PARAMETER

     

现有凭据中的某些字段无法更改。如果字段与受保护的值不匹配,则会返回此错误   现有凭证的字段。

但是,我在CREDENTIAL实例中放置的值是新凭据,而不是Windows凭据管理器中的现有凭据。

对于如何修复/改进的任何建议或想法表示赞赏。

===================================

应用FIX后更新:

新CredRead:

public boolean CredRead(
        String TargetName,
        int Type,
        int Flags,
        CREDENTIAL.ByReference Credential
        );

测试CredRead:

CREDENTIAL.ByReference pref = new CREDENTIAL.ByReference();
boolean ok = WinCrypt.instance.CredRead("TEST/account", WinCrypt.CRED_TYPE_GENERIC, 0, pref);
int rc = Kernel32.INSTANCE.GetLastError();
String errMsg = Kernel32Util.formatMessage(rc);
System.out.println("CredRead() - ok: " + ok + ", errno: " + rc + ", errmsg: " + errMsg);
System.out.println(String.format("Read username = '%s', password='%S' (%d bytes)\n",
        pref.UserName, pref.CredentialBlob, pref.CredentialBlobSize));

结果:

CredRead() - ok: true, errno: 0, errmsg: The operation completed successfully.
Read username = 'null', password='NULL' (0 bytes)

我检查了如何在contrib中使用ByReference的JNA示例,并且他们通过新建ByReference并传递给函数以相同的方式进行。

4 个答案:

答案 0 :(得分:1)

CredRead.PCREDENTIAL应为CREDENTIAL.ByReference。使用PointerByReference最终会传入指向NULL值的指针,而不是指向CREDENTIAL struct的预期指针。

CREDENTAL.CredentialBlob必须是PointerPointerType(如果您自己初始化该块,则可能为Memory)。使用内联字节数组将整个结构按数组大小移动,其中被调用者期望指向内存块的指针。

<强>更新

我想我误读了CredRead()的声明。

CredRead应继续使用PointerByReference。使用PointerByReference.getValue()提取&#34;返回&#34;来自CredRead()的指针值,以便根据指针创建新的CREDENTIALS实例。 PointerByReference.getPointer()为您提供分配用于保存指针值的内存地址。

public boolean CredWrite(
    CREDENTIAL Credential,
    int Flags
    );

public boolean CredRead(
    String TargetName,
    int Type,
    int Flags,
    PointerByReference pref
    );

PointerByReference pref = new PointerByReference()
CredRead(name, type, flags, pref);
creds = new Credentials(pref.getValue())

答案 1 :(得分:0)

如果查看CredRead()的WIN32定义,第四个参数是PCREDENTIAL *类型,即它是指向指针的指针。所以......

  • 您需要传入指针的地址,即4字节的内存块。
  • Windows分配一块内存来保存CREDENTIAL结构,然后通过将新内存块的地址放在传入的4字节块中来告诉您它在哪里。
  • 当您取消引用原始指针(传入CredRead()的那个)时,会得到另一个指针(4字节块),它本身需要被解引用才能到达CREDENTIAL。

欢迎来到C: - )

TL; DR:CREDENTIAL类需要像这样定义:

public static class CREDENTIAL extends Structure {
    public int Flags;
    public int Type;
    public WString TargetName;
    public WString Comment;
    public FILETIME LastWritten;
    public int CredentialBlobSize;
    public Pointer CredentialBlob; // <== discussed below
    public int Persist;
    public int AttributeCount;
    public Pointer Attributes;
    public WString TargetAlias;
    public WString UserName;
    private Pointer RawMemBlock; // <== discussed below

    public CREDENTIAL() { }

    public CREDENTIAL( Pointer ptr ) 
    { 
        // initialize ourself from the raw memory block returned to us by ADVAPI32
        super( ptr ) ; 
        RawMemBlock = ptr ; 
        read() ;
    }

    @Override
    protected void finalize()
    {    
        // clean up
        WinCrypt.INSTANCE.CredFree( RawMemBlock ) ;
    }

    @Override
    protected List<String> getFieldOrder()
    {
        return Arrays.asList( new String[] { "Flags" , "Type" , "TargetName" , "Comment" , "LastWritten" , "CredentialBlobSize" , "CredentialBlob" , "Persist" , "AttributeCount" , "Attributes" , "TargetAlias" , "UserName" } ) ;
    }
} ;

要致电CredRead(),请按以下方式声明:

public boolean CredRead( String target , int type , int flags , PointerByReference cred ) ;

并像这样调用它:

PointerByReference pptr = new PointerByReference() ;
boolean rc = WinCrypt.INSTANCE.CredRead( target , credType , 0 , pptr ) ;
if ( ! rc )
    ... ; // handle the error
CREDENTIAL cred = new CREDENTIAL( pptr.getValue() ) ;
String userName = cred.UserName.toString() ;
String password = new String( cred.CredentialBlob.getByteArray(0,cred.CredentialBlobSize) , "UTF-16LE" ) ;

凭据blob是Windows分配的另一个内存块,因此您不需要自己分配,Windows会这样做,并通过将其地址放在CredentialBlob字段中来告诉您它在哪里。

由于Windows已经为您分配了这些内存块,并且由于它无法知道您何时完成这些内存,因此您有责任释放它们。因此,CREDENTIAL构造函数保留原始指针CredRead()的副本,并在终结器中调用CredFree()来释放该内存。 CredFree()声明如下:

public void CredFree( Pointer cred ) ;

要保存凭证,您需要以CredWrite()期望的方式准备凭证blob,即在CREDENTIAL.CredentialBlob字段中存储指向它的指针:

// prepare the credential blob
byte[] credBlob = password.getBytes( "UTF-16LE" ) ;
Memory credBlobMem = new Memory( credBlob.length ) ;
credBlobMem.write( 0 , credBlob , 0 , credBlob.length ) ;

// create the credential
CREDENTIAL cred = new CREDENTIAL() ;
cred.Type = CRED_TYPE_GENERIC ;
cred.TargetName = new WString( target ) ;
cred.CredentialBlobSize = (int) credBlobMem.size() ;
cred.CredentialBlob = credBlobMem ;
cred.Persist = CRED_PERSIST_LOCAL_MACHINE ;
cred.UserName = new WString( userName ) ;

// save the credential
boolean rc = WinCrypt.INSTANCE.CredWrite( cred , 0 ) ;
if ( ! rc )
    ... ; // handle the error

作为附录,如果它是在服务帐户或任何其他没有永久配置文件的帐户下运行,所有这些都会遇到问题。我需要为通过任务计划程序运行的作业执行此操作,使用没有交互式登录权限的服务帐户,以及会发生什么:

  • 我创建了一个设置密码的批处理文件,并通过任务计划程序运行它(以便它在服务帐户下运行,密码进入正确的存储区)
  • Windows会创建一个临时配置文件(请检查“事件日志”),密码会进入该配置文件。
  • 另一个转储密码的批处理文件显示它们已成功设置。
  • 运行主作业,因为临时配置文件仍然存在,但是在5或10分钟后,Windows会删除它,包括您设置的密码: - /,因此下次运行主作业时,它因为密码不再存在而失败。

解决方案是创建永久性配置文件,理想情况是通过交互式登录,只需要执行一次。如果您不能这样做,可以programmatically执行此操作,尽管您需要管理员权限。

答案 2 :(得分:0)

Microsoft提供了一个MIT许可的Java库,用于访问VSTS令牌。 https://github.com/microsoft/vsts-authentication-library-for-java

它们在此处提供到凭证管理器功能和用法的JNA映射: https://github.com/microsoft/vsts-authentication-library-for-java/tree/master/storage/src/main/java/com/microsoft/alm/storage/windows/internal

如果您从头开始,非常有帮助。

答案 3 :(得分:0)

基于塔卡(taka)的回答,但出于以下考虑,我实施了一个完整的示例。

已考虑以下其他更正和方面:

  • 在CredReadW中,targetName必须为WString类型,而不是String类型。使用CredWriteW将数据写入Windows时,已经使用了WString。
  • 简化:我没有使用getFieldOrder()方法,而是使用了注释样式。
  • 不建议直接读取Kernel32 GetLastError,因为JNA可能会调用其他删除前一个LastError的调用。如How to make GetLastError reliably work with JNA?中所述,我更改为捕获最后一个错误作为异常。
  • 如taka所示,不需要在credentialBlobSize上添加“ 1+”-但是使用实内存或byte []大小更为重要,因为由于UTF-8,字符串长度包含的字符更少编码,而不是用于Windows API函数的UTF-16LE编码所产生的内存。

完整样本: (请注意,它需要JNA库;我正在使用https://github.com/java-native-access/jna上的JNA版本5.6.0)

package at.christoph-bimminger.sample;

import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.List;

import com.sun.jna.LastErrorException;
import com.sun.jna.Library;
import com.sun.jna.Memory;
import com.sun.jna.Native;
import com.sun.jna.Platform;
import com.sun.jna.Pointer;
import com.sun.jna.Structure;
import com.sun.jna.Structure.FieldOrder;
import com.sun.jna.WString;
import com.sun.jna.ptr.PointerByReference;

public class Main {


    public interface WinCrypt extends Library {

        WinCrypt INSTANCE = (WinCrypt) Native.load("Advapi32", WinCrypt.class);

        boolean CredWriteW(CREDENTIAL.ByReference credentialw, int flags) throws LastErrorException;

        boolean CredReadW(WString TargetName, int Type, int Flags, PointerByReference pptr) throws LastErrorException;

        public static final class Type {
            /**
             * The credential is a generic credential. The credential will not be used by
             * any particular authentication package. The credential will be stored securely
             * but has no other significant characteristics.
             */
            final static int CRED_TYPE_GENERIC = 1;

            /**
             * The credential is a password credential and is specific to Microsoft's
             * authentication packages. The NTLM, Kerberos, and Negotiate authentication
             * packages will automatically use this credential when connecting to the named
             * target.
             */
            final static int CRED_TYPE_DOMAIN_PASSWORD = 2;

            /**
             * The credential is a certificate credential and is specific to Microsoft's
             * authentication packages. The Kerberos, Negotiate, and Schannel authentication
             * packages automatically use this credential when connecting to the named
             * target.
             * 
             */
            final static int CRED_TYPE_DOMAIN_CERTIFICATE = 3;

            /**
             * This value is no longer supported. Windows Server 2003 and Windows XP: The
             * credential is a password credential and is specific to authentication
             * packages from Microsoft. The Passport authentication package will
             * automatically use this credential when connecting to the named target.
             * 
             * Additional values will be defined in the future. Applications should be
             * written to allow for credential types they do not understand.
             * 
             */
            final static int CRED_TYPE_DOMAIN_VISIBLE_PASSWORD = 4;

            /**
             * The credential is a certificate credential that is a generic authentication
             * package. Windows Server 2008, Windows Vista, Windows Server 2003 and Windows
             * XP: This value is not supported.
             */
            final static int CRED_TYPE_GENERIC_CERTIFICATE = 5;

            /**
             * The credential is supported by extended Negotiate packages. Windows Server
             * 2008, Windows Vista, Windows Server 2003 and Windows XP: This value is not
             * supported.
             * 
             */
            final static int CRED_TYPE_DOMAIN_EXTENDED = 6;

            /**
             * The maximum number of supported credential types.Windows Server 2008, Windows
             * Vista, Windows Server 2003 and Windows XP: This value is not supported.
             * 
             */
            final static int CRED_TYPE_MAXIMUM = 7;

            final static int CRED_TYPE_MAXIMUM_EX = CRED_TYPE_MAXIMUM + 1000;
        }

        public static final class Persist {
            final static int CRED_PERSIST_SESSION = 1;
            final static int CRED_PERSIST_LOCAL_MACHINE = 2;
            final static int CRED_PERSIST_ENTERPRISE = 3;
        }

    }

    /**
     * Representation of native struct FILETIME. See
     * https://docs.microsoft.com/en-us/windows/win32/api/minwinbase/ns-minwinbase-filetime
     * 
     * @author Christoph Bimminger
     *
     */
    @FieldOrder({ "dwLowDateTime", "dwHighDateTime" })
    public static final class FILETIME extends Structure {
        public int dwLowDateTime;
        public int dwHighDateTime;
    }

    /**
     * Representation of native struct CREDENTIALW. See
     * https://docs.microsoft.com/en-us/windows/win32/api/wincred/ns-wincred-credentialw
     * 
     * @author Christoph Bimminger
     *
     */
    @FieldOrder({ "flags", "type", "targetName", "comment", "lastWritten", "credentialBlobSize", "credentialBlob",
            "persist", "attributeCount", "attributes", "targetAlias", "userName" })
    public static class CREDENTIAL extends Structure {
        public int flags;
        public int type;
        public WString targetName;
        public WString comment;
        public FILETIME lastWritten;
        public int credentialBlobSize = 256;
        public Pointer credentialBlob;
        public int persist;
        public int attributeCount;
        public CREDENTIAL_ATTRIBUTE.ByReference attributes;
        public WString targetAlias;
        public WString userName;

        public static class ByReference extends CREDENTIAL implements Structure.ByReference {
            public ByReference() {
            }

            public ByReference(Pointer memory) {
                super(memory); // LINE 55
            }
        }

        public CREDENTIAL() {
            super();
        }

        public CREDENTIAL(Pointer memory) {
            super(memory);
            read(); // LINE 65
        }

    }

    public static class CREDENTIAL_ATTRIBUTE extends Structure {
        public String Keyword;
        public int Flags;
        public int ValueSize;
        public byte[] Value = new byte[128];

        public static class ByReference extends CREDENTIAL_ATTRIBUTE implements Structure.ByReference {
        }

        @Override
        protected List<String> getFieldOrder() {
            return Arrays.asList(new String[] { "Keyword", "Flags", "ValueSize", "Value" });
        }
    }

    public static void main(String[] args) throws UnsupportedEncodingException {
        if (!Platform.isWindows())
            throw new UnsatisfiedLinkError("This sample requires a windows environment, it uses wincred.h");

        { // --- SAVE
            String password = "brillant";

            // prepare the credential blob
            byte[] credBlob = password.getBytes("UTF-16LE");
            Memory credBlobMem = new Memory(credBlob.length);
            credBlobMem.write(0, credBlob, 0, credBlob.length);

            int cbCreds = credBlob.length;

            CREDENTIAL.ByReference cred = new CREDENTIAL.ByReference();
            cred.type = WinCrypt.Type.CRED_TYPE_GENERIC;
            cred.targetName = new WString("FOO/account");
            cred.credentialBlobSize = cbCreds;
            cred.credentialBlob = credBlobMem;
            cred.persist = WinCrypt.Persist.CRED_PERSIST_LOCAL_MACHINE;
            cred.userName = new WString("paula");

            try {
                boolean ok = WinCrypt.INSTANCE.CredWriteW(cred, 0);
            } catch (LastErrorException error) {
                int rc = error.getErrorCode();
                String errMsg = error.getMessage();
                System.out.println(rc + ": " + errMsg);
                System.exit(1);

            }
        }

        ///////////////////// READ PASS

        try {
            PointerByReference pptr = new PointerByReference();
            boolean ok = WinCrypt.INSTANCE.CredReadW(new WString("FOO/account"), WinCrypt.Type.CRED_TYPE_GENERIC, 0,
                    pptr);
            CREDENTIAL cred = new CREDENTIAL(pptr.getValue());

            String password = new String(cred.credentialBlob.getByteArray(0, cred.credentialBlobSize), "UTF-16LE");

            System.out.println(password);
        } catch (LastErrorException error) {
            int rc = error.getErrorCode();
            String errMsg = error.getMessage();
            System.out.println(rc + ": " + errMsg);
            System.exit(1);

        }

    }

}